Some checks failed
Check / eval (push) Has been cancelled
Every submenu's "↩ Back" row rendered two arrows: the ↩ glyph in the label plus a themed go-previous icon. back() now emits the plain label only — identical to what pick-lists already append — so Back shows a single glyph everywhere. BACK="↩ Back" stays the universal label/match, so no case-branch comparison changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1005 lines
48 KiB
Nix
1005 lines
48 KiB
Nix
# 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;
|
||
|
||
# App-launcher theme (SUPER+Space/D → `rofi -show drun -theme launcher`).
|
||
# Kept separate from the base theme so a theme can give the launcher its
|
||
# own layout (e.g. Boreal's icon grid) WITHOUT that layout leaking into
|
||
# the text menus, which share the base theme and need a list. The default
|
||
# just re-imports the active base theme (custom.rasi for generated themes,
|
||
# rofi.rasi for whole-swaps — see the dataFile below), so every other
|
||
# theme's launcher is byte-for-byte its list.
|
||
launcherOverride = cfg.themesDir + "/${t.slug}/launcher.rasi";
|
||
hasLauncherOverride = builtins.pathExists launcherOverride;
|
||
|
||
# ── Visual theme picker ──────────────────────────────────────────────
|
||
# Every preset (themes/<slug>.json) is read at eval time to build a grid
|
||
# of real desktop previews. Grouped dark-first then light — the previews
|
||
# make the mode obvious at a glance, so no light/dark submenu split — and
|
||
# the active theme is marked ✓. "Active" is just t.slug: every switch
|
||
# rebuilds this script, so the baked-in mark always tracks the live theme.
|
||
themeFiles = lib.filterAttrs
|
||
(n: ty: ty == "regular" && lib.hasSuffix ".json" n)
|
||
(builtins.readDir cfg.themesDir);
|
||
themeData = lib.mapAttrsToList (fname: _:
|
||
let
|
||
j = builtins.fromJSON (builtins.readFile (cfg.themesDir + "/${fname}"));
|
||
slug = j.slug or (lib.removeSuffix ".json" fname);
|
||
in {
|
||
inherit slug;
|
||
name = j.name or slug;
|
||
mode = j.mode or "dark";
|
||
hasPreview = builtins.pathExists (cfg.themesDir + "/${slug}/preview.png");
|
||
}) themeFiles;
|
||
|
||
byName = lib.sort (a: b: a.name < b.name);
|
||
orderedThemes =
|
||
byName (lib.filter (th: th.mode != "light") themeData) # dark group
|
||
++ byName (lib.filter (th: th.mode == "light") themeData); # then light
|
||
|
||
# rofi's element-icon cells are SQUARE, so a 16:9 preview would letterbox
|
||
# (theme-coloured bands top/bottom). Build-time crop each committed 480×270
|
||
# preview to a centred square so it fills the cell edge-to-edge. The source
|
||
# preview.png stays 480×270 (untouched) — only the displayed thumb is square.
|
||
themeThumbs = pkgs.runCommand "nomarchy-theme-thumbs"
|
||
{ nativeBuildInputs = [ pkgs.imagemagick ]; } ''
|
||
mkdir -p $out
|
||
${lib.concatMapStringsSep "\n"
|
||
(th: lib.optionalString th.hasPreview ''
|
||
magick ${cfg.themesDir + "/${th.slug}/preview.png"} \
|
||
-resize 360x360^ -gravity center -extent 360x360 -strip $out/${th.slug}.png
|
||
'')
|
||
orderedThemes}
|
||
'';
|
||
|
||
# One grid row per theme (square thumb + name, ✓ on the active one), and a
|
||
# Name→slug map so the picked label resolves back to the preset that
|
||
# nomarchy-theme-sync applies (pretty names ≠ slugs, hence the map). Themes
|
||
# with no preview degrade to a plain-name row.
|
||
themeRows = lib.concatMapStringsSep "\n" (th:
|
||
let label = th.name + lib.optionalString (th.slug == t.slug) " ✓";
|
||
in if th.hasPreview
|
||
then "row ${lib.escapeShellArg label} ${themeThumbs}/${th.slug}.png"
|
||
else "printf '%s\\n' ${lib.escapeShellArg label}")
|
||
orderedThemes;
|
||
themeSlugMap = lib.concatMapStringsSep "\n" (th:
|
||
" [${lib.escapeShellArg th.name}]=${lib.escapeShellArg th.slug}")
|
||
orderedThemes;
|
||
|
||
# Per-invocation grid layout (cards: a 16:9 preview above a centered name),
|
||
# overriding the single-column list theme just for this menu.
|
||
# · The icon box is sized 16:9 (rofi 2.0 takes a two-value `size`) to
|
||
# match the 480×270 previews exactly — they fill it with no letterbox.
|
||
# · The window is sized to the *content* (3 × icon + paddings), not a
|
||
# percentage, so a column is no wider than its card — otherwise the
|
||
# selection highlight balloons out around the image.
|
||
# · flow: horizontal lays cards out row-by-row (rofi's default Vertical
|
||
# fills column-by-column, so Down at a column's foot jumped to the top
|
||
# of the next column instead of scrolling the page).
|
||
# Grid dial — the previews are as big as the icon px allows; screen *height*
|
||
# caps it, so showing 2 rows (not 3) lets each card grow a lot. Scrolling
|
||
# reaches the rest. Tune themeGridIconW for size; the window resizes to fit.
|
||
# rofi's element-icon `size` is a SINGLE value (the manual only documents
|
||
# one) — a two-value "WxH" is silently collapsed, which is why the preview
|
||
# used to render tiny inside a too-wide column. So: size = the card *width*,
|
||
# rofi fits the 16:9 image tight to it (no square letterbox), and the window
|
||
# is sized so a column is exactly that width — the preview fills the cell.
|
||
themeGridCols = 3;
|
||
themeGridLines = 3;
|
||
themeGridIconW = 240; # square preview card side (px) — the size knob
|
||
themeGridPad = 0; # margin around the preview inside its cell (px)
|
||
themeGridGap = 8; # gap between cards (px)
|
||
themeGridThemeStr = lib.escapeShellArg (lib.concatStringsSep " " [
|
||
# window width = cards + their padding + inter-card gaps + chrome (~20px)
|
||
"window { width: ${toString (themeGridCols * (themeGridIconW + 2 * themeGridPad) + (themeGridCols - 1) * themeGridGap + 20)}px; }"
|
||
"listview { columns: ${toString themeGridCols}; lines: ${toString themeGridLines}; spacing: ${toString themeGridGap}px; flow: horizontal; }"
|
||
"element { orientation: vertical; padding: ${toString themeGridPad}px; spacing: 2px; }"
|
||
"element-icon { size: ${toString themeGridIconW}px; }"
|
||
"element-text { horizontal-align: 0.5; }"
|
||
]);
|
||
|
||
# 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;
|
||
# ? already implies Shift — the bind needs SHIFT in its modmask
|
||
# (keybinds.nix), but showing it would read double, so collapse
|
||
# "SHIFT + ?" back to the documented "?".
|
||
in lib.replaceStrings [ "SHIFT + ?" ] [ "?" ] (prefix + key);
|
||
cheatRows =
|
||
map (b: padRight 22 (prettyKeys b) + b.desc) (keybinds.binds
|
||
++ lib.optionals (lib.hasInfix "," cfg.keyboard.layout)
|
||
keybinds.multiLayoutBinds)
|
||
# Launch-or-focus binds (nomarchy.launchOrFocus, generated in
|
||
# hyprland.nix) — same renderer, so they land in the cheatsheet too.
|
||
++ map (e: padRight 22 (prettyKeys e)
|
||
+ (if e.desc != "" then e.desc
|
||
else "Focus or launch ${if e.command == "" then lib.toLower e.class else e.command}"))
|
||
cfg.launchOrFocus
|
||
++ map (e: padRight 22 e.keys + e.desc) keybinds.extra;
|
||
cheatsheetFile = pkgs.writeText "nomarchy-keybinds.txt"
|
||
(lib.concatStringsSep "\n" cheatRows + "\n");
|
||
|
||
# Dimmed keybind hints on menu rows (item 28c) — the same keybinds.nix
|
||
# + prettyKeys the cheatsheet uses, so they can never drift from the
|
||
# live binds. Pango spans; the menus that show them pass -markup-rows.
|
||
# Empty (no hint) when nothing binds the action.
|
||
hintForAction = action:
|
||
let m = lib.filter (b: b.action == action) keybinds.binds;
|
||
in lib.optionalString (m != [ ])
|
||
" <span size='small' alpha='55%'>${prettyKeys (lib.head m)}</span>";
|
||
menuHint = module: hintForAction "exec, nomarchy-menu ${module}";
|
||
# Apps opens rofi drun directly, not a nomarchy-menu module.
|
||
appsHint = " <span size='small' alpha='55%'>SUPER + D</span>";
|
||
|
||
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. Menu entries carry real icons from the theme's
|
||
# icon set (Papirus) via rofi's per-row icon protocol — see row() below.
|
||
# `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"
|
||
}
|
||
|
||
# Emit one dmenu row with a themed icon: `row "Label" icon-name`. The NUL
|
||
# + 0x1f field separator is rofi's per-row icon protocol; printed straight
|
||
# to the pipe because bash can't store NUL in a variable. Rendered when
|
||
# rofi has show-icons (on globally; passed explicitly on the icon menus).
|
||
row() { printf '%s\0icon\x1f%s\n' "$1" "$2"; }
|
||
|
||
# Back-audit exceptions (item 24): external modi/tools (calc, emoji,
|
||
# networkmanager_dmenu, rofi-pulse-select) can't take an injected row —
|
||
# Esc is their back path; free-text prompts (web, ask) have no list to
|
||
# append to — Esc cancels. Everything hand-rolled gets the row below.
|
||
# General menu convention: every list menu ends with a "↩ Back" entry that
|
||
# returns one level up (so you never have to Esc out and reopen). The `↩`
|
||
# glyph in the label IS the arrow — `back` must not also attach a
|
||
# go-previous icon, or icon menus render two arrows (item 34). So `back`
|
||
# is now just the plain-label emit, identical to what pick-lists append —
|
||
# one arrow everywhere. Always matched EXACTLY ("↩ Back") so it can't
|
||
# collide with clipboard or filename content.
|
||
BACK="↩ Back"
|
||
back() { printf '%s\n' "$BACK"; }
|
||
|
||
case "''${1:-}" in
|
||
power)
|
||
choice=$( {
|
||
row "Lock" system-lock-screen
|
||
row "Logout" system-log-out
|
||
row "Suspend" system-suspend
|
||
row "Hibernate" system-hibernate
|
||
row "Reboot" system-reboot
|
||
row "Shutdown" system-shutdown
|
||
back
|
||
} | rofi -dmenu -show-icons -p Power) || exit 0
|
||
case "$choice" in
|
||
"$BACK") exec "$0" ;;
|
||
*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'; printf '%s\n' "$BACK"; } \
|
||
| rofi -dmenu -p "profile (now: $cur)") || exit 0
|
||
[ "$choice" = "$BACK" ] && exec "$0" system
|
||
[ -n "$choice" ] && powerprofilesctl set "$choice" ;;
|
||
|
||
theme)
|
||
# Visual picker: a grid of real desktop previews (rows + Name→slug
|
||
# map generated from the presets at build time). Pick a card → apply
|
||
# that slug. The grid layout is a per-invocation -theme-str override.
|
||
declare -A THEME_SLUG=(
|
||
${themeSlugMap}
|
||
)
|
||
choice=$( {
|
||
${themeRows}
|
||
back
|
||
} | rofi -dmenu -show-icons -p Theme -theme-str ${themeGridThemeStr}) || exit 0
|
||
[ "$choice" = "$BACK" ] && exec "$0"
|
||
choice="''${choice% ✓}" # drop the active marker if present
|
||
slug="''${THEME_SLUG[$choice]:-}"
|
||
[ -n "$slug" ] && exec nomarchy-theme-sync apply "$slug" ;;
|
||
|
||
clipboard)
|
||
sel=$( { cliphist list; printf '%s\n' "$BACK"; } | rofi -dmenu -p clip) || exit 0
|
||
[ "$sel" = "$BACK" ] && exec "$0" tools
|
||
printf '%s' "$sel" | cliphist decode | wl-copy ;;
|
||
|
||
calc)
|
||
# Live calculator (rofi-calc): re-evaluates every keystroke via
|
||
# libqalculate — which dodges the qalc CLI's "15% of 200" misparse —
|
||
# with the answer as the top entry. Enter copies the result; the
|
||
# menu stays open to chain calculations (Esc closes).
|
||
exec rofi -show calc -modi calc -no-show-match -no-sort \
|
||
-calc-command "echo -n '{result}' | wl-copy" ;;
|
||
|
||
files)
|
||
sel=$( { fd . "$HOME" --type f --hidden --exclude .git --exclude .cache 2>/dev/null \
|
||
| head -n 50000 | sed "s|^$HOME/||"; printf '%s\n' "$BACK"; } \
|
||
| rofi -dmenu -p file) || exit 0
|
||
[ "$sel" = "$BACK" ] && exec "$0" tools
|
||
[ -n "$sel" ] && exec xdg-open "$HOME/$sel" ;;
|
||
|
||
emoji)
|
||
# Emoji / symbol picker (rofi-emoji). The default action copies the
|
||
# glyph to the clipboard through the plugin's Wayland adapter
|
||
# (wl-copy); the alternate action types it into the focused window.
|
||
exec rofi -show emoji -modi emoji ;;
|
||
|
||
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)
|
||
# Native rofi wifi/VPN picker (networkmanager_dmenu) over the system
|
||
# NetworkManager — replaces the old nmtui-in-a-terminal flow. Reads
|
||
# its config from xdg.configFile below (told to drive rofi).
|
||
# External tool: no injected ↩ Back row possible — Esc closes.
|
||
exec networkmanager_dmenu ;;
|
||
|
||
vpn)
|
||
# VPN setup + management (nomarchy-vpn): NM VPN/WireGuard connect +
|
||
# config import + Tailscale. Distinct from `network` (the wifi picker).
|
||
exec nomarchy-vpn ;;
|
||
|
||
bluetooth)
|
||
# blueman-manager GUI (services.blueman.enable, system-side).
|
||
exec blueman-manager ;;
|
||
|
||
printers)
|
||
# system-config-printer GUI (the CUPS admin app), installed by
|
||
# nomarchy.services.printing. Self-gated in the System menu; guard
|
||
# here too in case it's invoked directly.
|
||
command -v system-config-printer >/dev/null 2>&1 \
|
||
&& exec system-config-printer
|
||
notify-send "Printers" "Not available (nomarchy.services.printing off?)."; exit 0 ;;
|
||
|
||
audio)
|
||
# PipeWire (pulse) sink/source switcher via rofi-pulse-select. Self-
|
||
# gated in the System menu on the pulse socket; guarded here too.
|
||
if [ ! -e "''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/pulse/native" ]; then
|
||
notify-send "Audio" "No PipeWire/PulseAudio socket (nomarchy.audio off?)."; exit 0
|
||
fi
|
||
choice=$( {
|
||
row "Output device" audio-volume-high
|
||
row "Input device" audio-input-microphone
|
||
back
|
||
} | rofi -dmenu -show-icons -p Audio) || exit 0
|
||
case "$choice" in
|
||
"$BACK") exec "$0" system ;;
|
||
*Output*) exec rofi-pulse-select sink ;;
|
||
*Input*) exec rofi-pulse-select source ;;
|
||
esac ;;
|
||
|
||
display)
|
||
# Per-output resolution picker. Applies the chosen mode INSTANTLY via
|
||
# `hyprctl keyword monitor` — keeping the output's current position and
|
||
# scale, so only the resolution changes — and PERSISTS it to the in-flake
|
||
# state (settings.monitors.<name>, --no-switch = no rebuild), so the
|
||
# choice survives logout/reboot. A later rebuild bakes the same
|
||
# resolution into the generated monitor rule (hyprland.nix overlays
|
||
# settings.monitors onto nomarchy.monitors by name). Self-gated to the
|
||
# Hyprland session; guard here too in case it's invoked directly.
|
||
command -v hyprctl >/dev/null 2>&1 \
|
||
|| { notify-send "Display" "Hyprland is not running."; exit 0; }
|
||
mons=$(hyprctl monitors -j)
|
||
|
||
# Choose the output (skip the chooser when only one is connected and
|
||
# there are no profiles — the Profiles row needs the chooser to live in).
|
||
profs=; command -v nomarchy-display-profile >/dev/null 2>&1 && profs=1
|
||
if [ -n "$profs" ] || [ "$(printf '%s' "$mons" | jq -r '.[].name' | grep -c .)" -gt 1 ]; then
|
||
name=$( {
|
||
[ -n "$profs" ] && printf 'Profiles ›\n'
|
||
printf '%s' "$mons" | jq -r '.[] | "\(.name) · \(.width)x\(.height)@\(.refreshRate|round)Hz"'
|
||
printf '%s\n' "$BACK"
|
||
} | rofi -dmenu -p Display ) || exit 0
|
||
[ "$name" = "$BACK" ] && exec "$0" system
|
||
[ "$name" = "Profiles ›" ] && exec "$0" display-profile
|
||
name=''${name%% ·*} # strip the " · WxH@R" hint
|
||
else
|
||
name=$(printf '%s' "$mons" | jq -r '.[0].name')
|
||
fi
|
||
[ -n "$name" ] || exit 0
|
||
|
||
# Pick a mode: a few Hyprland resolution tokens, then the output's
|
||
# advertised modes (rounded refresh rate, deduped, highest first). The
|
||
# prompt shows the current mode.
|
||
cur=$(printf '%s' "$mons" | jq -r --arg n "$name" \
|
||
'.[]|select(.name==$n)|"\(.width)x\(.height)@\(.refreshRate|round)"')
|
||
mode=$( {
|
||
printf 'preferred\nhighres\nhighrr\n'
|
||
printf '%s' "$mons" | jq -r --arg n "$name" '.[]|select(.name==$n)|.availableModes[]' \
|
||
| sed 's/Hz$//' | awk -F@ '{ printf "%s@%d\n", $1, $2 + 0.5 }' | sort -rV -u
|
||
printf '%s\n' "$BACK"
|
||
} | rofi -dmenu -p "$name (now $cur)" ) || exit 0
|
||
[ "$mode" = "$BACK" ] && exec "$0" display
|
||
[ -n "$mode" ] || exit 0
|
||
|
||
# Apply live (keep the output's current position + scale), then persist.
|
||
pos=$(printf '%s' "$mons" | jq -r --arg n "$name" '.[]|select(.name==$n)|"\(.x)x\(.y)"')
|
||
scale=$(printf '%s' "$mons" | jq -r --arg n "$name" '.[]|select(.name==$n)|.scale')
|
||
if hyprctl keyword monitor "$name,$mode,$pos,$scale" >/dev/null 2>&1; then
|
||
nomarchy-theme-sync --quiet set "settings.monitors.$name" "$mode" --no-switch
|
||
notify-send "Display" "$name → $mode"
|
||
else
|
||
notify-send "Display" "Could not set $name to $mode."
|
||
fi ;;
|
||
|
||
capture)
|
||
# Screenshots + screen recording. Recording rows self-swap: while
|
||
# one runs, the only offer is Stop (same surface as the bar's ⏺).
|
||
choice=$( {
|
||
row "Region → clipboard" applets-screenshooter
|
||
row "Region → file" applets-screenshooter
|
||
row "Full screen → clipboard" camera-photo
|
||
row "Full screen → file" camera-photo
|
||
command -v tesseract >/dev/null 2>&1 && row "OCR region → clipboard" scanner
|
||
if command -v nomarchy-record >/dev/null 2>&1; then
|
||
if nomarchy-record active; then
|
||
row "■ Stop recording" media-playback-stop
|
||
else
|
||
row "Record region" media-record
|
||
row "Record screen" media-record
|
||
row "Record region + audio" audio-input-microphone
|
||
row "Record screen + audio" audio-input-microphone
|
||
fi
|
||
fi
|
||
back
|
||
} | rofi -dmenu -show-icons -p Capture) || exit 0
|
||
dir="$HOME/Pictures/Screenshots"
|
||
file="$dir/$(date +%Y%m%d-%H%M%S).png"
|
||
case "$choice" in
|
||
"$BACK") exec "$0" tools ;;
|
||
*"Region → clipboard"*) grim -g "$(slurp)" - | wl-copy ;;
|
||
*"Region → file"*) mkdir -p "$dir"; grim -g "$(slurp)" "$file" \
|
||
&& notify-send "Screenshot saved" "$file" ;;
|
||
*"OCR region"*)
|
||
# Select → screenshot → tesseract → clipboard. Esc in slurp
|
||
# cancels silently (the recording.nix pattern); recognizing
|
||
# nothing toasts instead of copying an empty clipboard.
|
||
geo=$(slurp) || exit 0
|
||
txt=$(grim -g "$geo" - | tesseract stdin stdout 2>/dev/null)
|
||
[ -n "''${txt//[[:space:]]/}" ] \
|
||
|| { notify-send "OCR" "No text recognized in that region."; exit 0; }
|
||
printf '%s' "$txt" | wl-copy
|
||
notify-send "OCR" "Copied $(printf '%s' "$txt" | wc -w) word(s) to the clipboard." ;;
|
||
*"Full screen → clipboard"*) grim - | wl-copy ;;
|
||
*"Full screen → file"*) mkdir -p "$dir"; grim "$file" \
|
||
&& notify-send "Screenshot saved" "$file" ;;
|
||
*"Stop recording"*) exec nomarchy-record stop ;;
|
||
*"Record region + audio"*) exec nomarchy-record start region audio ;;
|
||
*"Record screen + audio"*) exec nomarchy-record start screen audio ;;
|
||
*"Record region"*) exec nomarchy-record start region ;;
|
||
*"Record screen"*) exec nomarchy-record start screen ;;
|
||
esac ;;
|
||
|
||
lookfeel)
|
||
# Look & Feel — appearance grouped (item 19): the theme picker,
|
||
# wallpaper cycling, night light. The root stays six rows; the
|
||
# SUPER+T / SUPER+SHIFT+T direct binds still hit the leaves.
|
||
choice=$( {
|
||
row "Theme${menuHint "theme"}" preferences-desktop-theme
|
||
row "Next wallpaper${hintForAction "exec, nomarchy-theme-sync bg next"}" preferences-desktop-wallpaper
|
||
if systemctl --user is-active --quiet hyprsunset.service 2>/dev/null
|
||
then row "Night light (on)" weather-clear-night
|
||
else row "Night light (off)" weather-clear-night
|
||
fi
|
||
back
|
||
} | rofi -dmenu -show-icons -markup-rows -p "Look & Feel") || exit 0
|
||
case "$choice" in
|
||
"$BACK") exec "$0" ;;
|
||
*Theme*) exec "$0" theme ;;
|
||
*wallpaper*) exec nomarchy-theme-sync bg next ;;
|
||
*"Night light"*) exec "$0" nightlight ;;
|
||
esac ;;
|
||
|
||
display-profile)
|
||
# Named multi-output layouts (nomarchy.displayProfiles). Applying is
|
||
# instant (hyprctl per entry, via nomarchy-display-profile) and
|
||
# persisted in-flake (settings.displayProfile) so the next rebuild
|
||
# bakes it. "Base layout" clears the state and hyprctl-reloads.
|
||
command -v nomarchy-display-profile >/dev/null 2>&1 \
|
||
|| { notify-send "Display profiles" "None declared (nomarchy.displayProfiles)."; exit 0; }
|
||
active=$(nomarchy-display-profile active)
|
||
choice=$( {
|
||
nomarchy-display-profile list | while IFS= read -r p; do
|
||
if [ "$p" = "$active" ]; then printf '● %s\n' "$p"; else printf '○ %s\n' "$p"; fi
|
||
done
|
||
printf 'Base layout\n'
|
||
if [ "$(nomarchy-theme-sync get settings.displayProfileAuto 2>/dev/null)" = true ]
|
||
then printf 'Auto-switch (on)\n'
|
||
else printf 'Auto-switch (off)\n'
|
||
fi
|
||
printf '%s\n' "$BACK"
|
||
} | rofi -dmenu -p Profiles ) || exit 0
|
||
case "$choice" in
|
||
"$BACK") exec "$0" display ;;
|
||
"Base layout") exec nomarchy-display-profile base ;;
|
||
*"Auto-switch"*)
|
||
# Instant in-flake flag; the watcher reads it live on the next
|
||
# output change — no rebuild, no unit juggling.
|
||
if [ "$(nomarchy-theme-sync get settings.displayProfileAuto 2>/dev/null)" = true ]; then
|
||
nomarchy-theme-sync --quiet set settings.displayProfileAuto false --no-switch
|
||
notify-send "Display profiles" "Auto-switch off — profiles change only from this menu."
|
||
else
|
||
nomarchy-theme-sync --quiet set settings.displayProfileAuto true --no-switch
|
||
notify-send "Display profiles" "Auto-switch on — plugging/unplugging outputs picks the matching profile."
|
||
fi
|
||
exec "$0" display-profile ;;
|
||
*) exec nomarchy-display-profile apply "''${choice#[●○] }" ;;
|
||
esac ;;
|
||
|
||
colorpicker)
|
||
# Pick a pixel's color (hyprpicker's zoom loupe) → clipboard, hex
|
||
# in the confirmation toast. Esc/cancel exits silently. Self-gated
|
||
# in the Tools row; guarded here too for direct invocation.
|
||
command -v hyprpicker >/dev/null 2>&1 \
|
||
|| { notify-send "Color picker" "hyprpicker is not installed."; exit 0; }
|
||
color=$(hyprpicker) || exit 0
|
||
[ -n "$color" ] || exit 0
|
||
printf '%s' "$color" | wl-copy
|
||
notify-send "Color picker" "$color copied to the clipboard." ;;
|
||
|
||
keybinds)
|
||
# Read-only cheatsheet, generated from keybinds.nix at build time.
|
||
# Enter on a row is a deliberate no-op (nothing to launch); ↩ Back
|
||
# returns to the root picker, which is also where it's launched.
|
||
sel=$( { cat ${cheatsheetFile}; printf '%s\n' "$BACK"; } \
|
||
| rofi -dmenu -i -p keys ) || exit 0
|
||
[ "$sel" = "$BACK" ] && exec "$0"
|
||
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" ;;
|
||
|
||
dnd)
|
||
# Toggle swaync Do-Not-Disturb (-d prints the new state). With DND
|
||
# on, notifications are suppressed — the Waybar bell-off icon is the
|
||
# cue — so only confirm with a toast when turning it back off.
|
||
[ "$(swaync-client -d)" = "false" ] \
|
||
&& notify-send "Do Not Disturb off" "Notifications resumed."
|
||
exit 0 ;;
|
||
|
||
nightlight)
|
||
# Force the scheduled blue-light filter on/off for the session by
|
||
# starting/stopping hyprsunset (nomarchy-nightlight, from nightlight.nix).
|
||
# Self-gated in the menu; guard here too in case it's invoked directly.
|
||
command -v nomarchy-nightlight >/dev/null 2>&1 \
|
||
&& exec nomarchy-nightlight toggle
|
||
notify-send "Night light" "Not available (nomarchy.nightlight off?)."; exit 0 ;;
|
||
|
||
autotimezone)
|
||
# Toggle automatic timezone detection (geoclue + automatic-timezoned).
|
||
# A SYSTEM service, so flipping it writes the in-flake flag and runs a
|
||
# system rebuild (sudo) + a home switch — in a terminal, like Snapshots.
|
||
command -v nomarchy-autotimezone >/dev/null 2>&1 \
|
||
|| { notify-send "Auto timezone" "Unavailable on this machine."; exit 0; }
|
||
exec ${cfg.terminal} -e nomarchy-autotimezone toggle ;;
|
||
|
||
autocommit)
|
||
# Toggle opt-in auto-commit: every menu/theme mutation also commits
|
||
# theme-state.json (that file only) in the downstream flake, so
|
||
# settings history is `git log`. Nothing in Nix consumes the flag —
|
||
# the tool reads the live state on each write — so the toggle is
|
||
# instant, no rebuild. The off-write commits too (the tool fires
|
||
# when the flag was on before OR after), keeping history consistent.
|
||
if [ "$(nomarchy-theme-sync get settings.autoCommit 2>/dev/null)" = true ]; then
|
||
nomarchy-theme-sync --quiet set settings.autoCommit false --no-switch
|
||
notify-send "Auto-commit" "Off — menu changes stay uncommitted in your flake."
|
||
else
|
||
nomarchy-theme-sync --quiet set settings.autoCommit true --no-switch
|
||
notify-send "Auto-commit" "On — each change commits theme-state.json in your flake."
|
||
fi
|
||
exit 0 ;;
|
||
|
||
snapshot)
|
||
# Snapshot browse/restore — the btrfs-assistant GUI via its pkexec
|
||
# launcher. The "2.2 segfaults on 26.05" diagnosis was HALF right:
|
||
# only UNPRIVILEGED runs crash (libbtrfsutil unprivileged subvolume
|
||
# iteration, btrfs-progs 6.17.1; fixed upstream after) — as root it
|
||
# runs fine, and the launcher runs it as root. The prompt needs the
|
||
# session polkit agent (hyprpolkitagent, exec-once in hyprland.nix).
|
||
# Fallback: nomarchy-snapshots, the keyboard-driven fzf browser in a
|
||
# terminal (kept installed — also nice over SSH).
|
||
# Self-gated to nomarchy.system.snapper, so it no-ops elsewhere.
|
||
command -v btrfs-assistant-launcher >/dev/null 2>&1 \
|
||
&& exec btrfs-assistant-launcher
|
||
command -v nomarchy-snapshots >/dev/null 2>&1 \
|
||
|| { notify-send "Snapshots" "Snapshot tools unavailable (nomarchy.system.snapper off?)."; exit 0; }
|
||
exec ${cfg.terminal} -e sudo nomarchy-snapshots ;;
|
||
|
||
doctor)
|
||
# Read-only health sheet in a terminal; exit code doesn't matter
|
||
# here — the sheet itself is the product.
|
||
exec ${cfg.terminal} -e sh -c "nomarchy-doctor
|
||
printf '\nEnter to close.'; read -r _" ;;
|
||
|
||
controlcenter)
|
||
# Nomarchy TUI Control Center.
|
||
exec ${cfg.terminal} -e nomarchy-control-center ;;
|
||
|
||
rollback)
|
||
# Undo, one menu away (informative + rock-stable). Desktop =
|
||
# Home Manager generations: theme/config history is already one
|
||
# generation per change, and running an older generation's
|
||
# activate script is the supported HM rollback — reversible by
|
||
# activating a newer one again, so no typed-yes gate here.
|
||
# System-level undo deliberately LINKS OUT instead of putting
|
||
# destructive flows in rofi: boot-menu generations and snapper
|
||
# rollback have their own homes and gates (docs/RECOVERY.md,
|
||
# nomarchy-snapshots).
|
||
gens=$(home-manager generations 2>/dev/null | head -10)
|
||
choice=$( {
|
||
if [ -n "$gens" ]; then
|
||
printf '%s\n' "$gens" | awk '{
|
||
printf "Desktop gen %s — %s %s%s\n", $5, $1, $2, (NR==1 ? " (current)" : "")
|
||
}' | while IFS= read -r line; do row "$line" document-open-recent; done
|
||
fi
|
||
command -v nomarchy-snapshots >/dev/null 2>&1 \
|
||
&& row "System files → Snapshots" timeshift
|
||
row "System config → boot an older generation (how)" help-about
|
||
back
|
||
} | rofi -dmenu -show-icons -p Rollback) || exit 0
|
||
case "$choice" in
|
||
"$BACK") exec "$0" system ;;
|
||
"Desktop gen "*)
|
||
num=''${choice#Desktop gen }; num=''${num%% *}
|
||
path=$(printf '%s\n' "$gens" | awk -v n="$num" '$5 == n { print $7 }')
|
||
[ -x "$path/activate" ] \
|
||
|| { notify-send "Rollback" "Generation $num not found on disk."; exit 1; }
|
||
exec ${cfg.terminal} -e sh -c "
|
||
echo \"Activating Home Manager generation $num…\"; echo
|
||
\"$path/activate\"
|
||
echo; echo \"Desktop rolled back to generation $num.\"
|
||
echo \"(Roll forward the same way — newer generations stay listed.)\"
|
||
printf 'Enter to close.'; read -r _" ;;
|
||
*Snapshots*) exec "$0" snapshot ;;
|
||
*"older generation"*)
|
||
notify-send "System rollback" "Reboot and pick an older NixOS generation in the boot menu (the last 10 are kept). Make it stick: revert the change in ~/.nomarchy, then sys-rebuild. Details: docs/RECOVERY.md §3." ;;
|
||
esac ;;
|
||
|
||
tools)
|
||
# Tools submenu — utilities you invoke. Each leaf is also reachable
|
||
# directly (SUPER+CTRL+<mnemonic>); this just keeps the root short.
|
||
choice=$( {
|
||
row "Calculator${menuHint "calc"}" accessories-calculator
|
||
row "Clipboard${menuHint "clipboard"}" edit-paste
|
||
row "Emoji${menuHint "emoji"}" face-smile-big
|
||
row "Files${menuHint "files"}" system-file-manager
|
||
row "Web search${menuHint "web"}" system-search
|
||
row "Capture${menuHint "capture"}" applets-screenshooter
|
||
command -v hyprpicker >/dev/null 2>&1 && row "Color picker${menuHint "colorpicker"}" preferences-desktop-color
|
||
row "Ask Claude${menuHint "ask"}" internet-chat
|
||
back
|
||
} | rofi -dmenu -show-icons -markup-rows -p Tools) || exit 0
|
||
case "$choice" in
|
||
"$BACK") exec "$0" ;;
|
||
*Calc*) exec "$0" calc ;;
|
||
*Clipboard*) exec "$0" clipboard ;;
|
||
*Emoji*) exec "$0" emoji ;;
|
||
*Files*) exec "$0" files ;;
|
||
*Web*) exec "$0" web ;;
|
||
*Capture*) exec "$0" capture ;;
|
||
*Color*) exec "$0" colorpicker ;;
|
||
*Ask*) exec "$0" ask ;;
|
||
esac ;;
|
||
|
||
system)
|
||
# System submenu — connectivity and machine state. Snapshots and the
|
||
# power-profile picker self-gate (BTRFS+snapper / laptop on ppd), so
|
||
# the submenu shrinks to match the hardware, like the Waybar modules.
|
||
bats=(/sys/class/power_supply/BAT*)
|
||
choice=$( {
|
||
row "Network${menuHint "network"}" network-wireless
|
||
row "VPN" network-vpn
|
||
row "Bluetooth${menuHint "bluetooth"}" bluetooth
|
||
[ -e "''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/pulse/native" ] \
|
||
&& row "Audio" audio-volume-high
|
||
row "Display" video-display
|
||
command -v system-config-printer >/dev/null 2>&1 && row "Printers" printer
|
||
row "Do Not Disturb${menuHint "dnd"}" notification-disabled
|
||
if [ "$(nomarchy-theme-sync get settings.autoTimezone 2>/dev/null)" = true ]
|
||
then row "Auto timezone (on)" preferences-system-time
|
||
else row "Auto timezone (off)" preferences-system-time
|
||
fi
|
||
if [ -e "''${NOMARCHY_PATH:-$HOME/.nomarchy}/.git" ]; then
|
||
if [ "$(nomarchy-theme-sync get settings.autoCommit 2>/dev/null)" = true ]
|
||
then row "Auto-commit (on)" git
|
||
else row "Auto-commit (off)" git
|
||
fi
|
||
fi
|
||
command -v nomarchy-snapshots >/dev/null 2>&1 && row "Snapshots" timeshift
|
||
row "Rollback" edit-undo
|
||
command -v nomarchy-doctor >/dev/null 2>&1 && row "Doctor" utilities-system-monitor
|
||
command -v nomarchy-control-center >/dev/null 2>&1 && row "Control Center" preferences-desktop
|
||
if [ -e "''${bats[0]}" ] && command -v powerprofilesctl >/dev/null 2>&1; then
|
||
row "Power profile" preferences-system-power
|
||
fi
|
||
back
|
||
} | rofi -dmenu -show-icons -markup-rows -p System) || exit 0
|
||
case "$choice" in
|
||
"$BACK") exec "$0" ;;
|
||
*Network*) exec "$0" network ;;
|
||
*VPN*) exec "$0" vpn ;;
|
||
*Bluetooth*) exec "$0" bluetooth ;;
|
||
*Audio*) exec "$0" audio ;;
|
||
*Display*) exec "$0" display ;;
|
||
*Printers*) exec "$0" printers ;;
|
||
*"Do Not Disturb"*) exec "$0" dnd ;;
|
||
*"Auto timezone"*) exec "$0" autotimezone ;;
|
||
*"Auto-commit"*) exec "$0" autocommit ;;
|
||
*Snapshots*) exec "$0" snapshot ;;
|
||
*Rollback*) exec "$0" rollback ;;
|
||
*Doctor*) exec "$0" doctor ;;
|
||
*"Control Center"*) exec "$0" controlcenter ;;
|
||
*"Power profile"*) exec "$0" power-profile ;;
|
||
esac ;;
|
||
|
||
"")
|
||
# Root picker — grouped into category submenus so the top level stays
|
||
# short and scannable. Apps/Theme/Power/Keybindings are frequent enough
|
||
# to keep at the top; everything else lives under Tools or System.
|
||
choice=$( {
|
||
row "Apps${appsHint}" applications-all
|
||
row "Look & Feel" preferences-desktop-theme
|
||
row "Tools" applications-utilities
|
||
row "System" preferences-system
|
||
row "Power${menuHint "power"}" system-shutdown
|
||
row "Keybindings${menuHint "keybinds"}" preferences-desktop-keyboard
|
||
} | rofi -dmenu -show-icons -markup-rows -p Menu) || exit 0
|
||
case "$choice" in
|
||
*Apps*) exec rofi -show drun -theme launcher ;;
|
||
*Look*) exec "$0" lookfeel ;;
|
||
*Tools*) exec "$0" tools ;;
|
||
*System*) exec "$0" system ;;
|
||
*Power*) exec "$0" power ;;
|
||
*Keybindings*) exec "$0" keybinds ;;
|
||
esac ;;
|
||
|
||
*)
|
||
echo "usage: nomarchy-menu [lookfeel|tools|system|power|power-profile|theme|clipboard|calc|files|emoji|web|network|bluetooth|audio|display|display-profile|printers|capture|colorpicker|keybinds|ask|dnd|nightlight|autotimezone|autocommit|vpn|snapshot]" >&2
|
||
exit 64 ;;
|
||
esac
|
||
'';
|
||
|
||
# VPN setup + management — a dedicated System submenu (and the custom/vpn
|
||
# Waybar indicator's click target). Goes past networkmanager_dmenu (which
|
||
# only connects existing VPNs) to importing configs and driving Tailscale:
|
||
# · NetworkManager VPN/WireGuard connections, ● active / ○ inactive, toggled
|
||
# up/down (networkmanager-group users need no sudo).
|
||
# · Import a WireGuard .conf or OpenVPN .ovpn (nmcli import; OpenVPN needs the
|
||
# networkmanager-openvpn plugin, shipped system-side).
|
||
# · Tailscale (self-gated on the CLI being present = nomarchy.services.tailscale,
|
||
# which makes the login user the operator): status is read-only; up/down/
|
||
# exit-node run inline without sudo thanks to the operator grant, falling
|
||
# back to a sudo terminal if it's absent. First login goes to a terminal.
|
||
# Secrets stay in NetworkManager's / Tailscale's own store — no new manager.
|
||
nomarchy-vpn = pkgs.writeShellScriptBin "nomarchy-vpn" ''
|
||
set -u
|
||
BACK="↩ Back"
|
||
|
||
conn_rows() {
|
||
nmcli -t -f NAME,TYPE,STATE connection show 2>/dev/null \
|
||
| awk -F: '$2=="vpn"||$2=="wireguard"{ printf "%s %s\n", ($3=="activated"?"●":"○"), $1 }'
|
||
}
|
||
|
||
toggle_conn() {
|
||
mark=''${1%% *}; name=''${1#* }
|
||
if [ "$mark" = "●" ]; then
|
||
nmcli connection down "$name" >/dev/null 2>&1 \
|
||
&& notify-send "VPN" "Disconnected $name" || notify-send "VPN" "Could not disconnect $name"
|
||
else
|
||
nmcli connection up "$name" >/dev/null 2>&1 \
|
||
&& notify-send "VPN" "Connected $name" || notify-send "VPN" "Could not connect $name (auth or bad config?)"
|
||
fi
|
||
}
|
||
|
||
import_config() {
|
||
file=$( { fd -e conf -e ovpn . "$HOME" --type f --hidden \
|
||
--exclude .git --exclude .cache 2>/dev/null \
|
||
| sed "s|^$HOME/||"; printf '%s\n' "$BACK"; } \
|
||
| rofi -dmenu -p "Import VPN config" ) || return
|
||
{ [ -z "$file" ] || [ "$file" = "$BACK" ]; } && return
|
||
case "$file" in *.ovpn) type=openvpn ;; *) type=wireguard ;; esac
|
||
if nmcli connection import type "$type" file "$HOME/$file" >/dev/null 2>&1; then
|
||
notify-send "VPN" "Imported $type config: $(basename "$file")"
|
||
else
|
||
notify-send "VPN" "Import failed ($type plugin missing, or bad file?)"
|
||
fi
|
||
}
|
||
|
||
ts_state() { tailscale status --json 2>/dev/null | jq -r '.BackendState // "Unknown"'; }
|
||
# Run a privileged tailscale subcommand. nomarchy.services.tailscale makes
|
||
# the login user the operator, so this runs inline (no sudo); if the operator
|
||
# grant is absent (downstream dropped it) it falls back to a sudo terminal.
|
||
ts_priv() {
|
||
if tailscale "$@" >/dev/null 2>&1; then notify-send "Tailscale" "tailscale $*"
|
||
else ${cfg.terminal} -e sudo tailscale "$@"; fi
|
||
}
|
||
ts_exit_node() {
|
||
node=$( { printf 'none\n'; tailscale exit-node list 2>/dev/null | awk 'NR>1 && $2 {print $2}'; \
|
||
printf '%s\n' "$BACK"; } | rofi -dmenu -p "Exit node" ) || return
|
||
{ [ -z "$node" ] || [ "$node" = "$BACK" ]; } && return
|
||
[ "$node" = none ] && node=""
|
||
ts_priv set --exit-node="$node"
|
||
}
|
||
tailscale_menu() {
|
||
while :; do
|
||
st=$(ts_state)
|
||
choice=$( {
|
||
if [ "$st" = Running ]; then printf 'Disconnect\n'; else printf 'Connect\n'; fi
|
||
printf 'Exit node…\n'
|
||
printf '%s\n' "$BACK"
|
||
} | rofi -dmenu -p "Tailscale ($st)" ) || return
|
||
case "$choice" in
|
||
"$BACK"|"") return ;;
|
||
# Authed-but-down → bring it up inline; otherwise it needs an
|
||
# interactive login, so show it in a terminal (URL visible).
|
||
Connect) if [ "$st" = Stopped ]; then ts_priv up; else ${cfg.terminal} -e sudo tailscale up; fi ;;
|
||
Disconnect) ts_priv down ;;
|
||
"Exit node…") ts_exit_node ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
while :; do
|
||
have_ts=""
|
||
command -v tailscale >/dev/null 2>&1 && have_ts=1
|
||
choice=$( {
|
||
conn_rows
|
||
printf 'Import config…\n'
|
||
[ -n "$have_ts" ] && printf 'Tailscale ›\n'
|
||
printf '%s\n' "$BACK"
|
||
} | rofi -dmenu -p VPN ) || exit 0
|
||
case "$choice" in
|
||
"$BACK"|"") exec nomarchy-menu system ;;
|
||
"Import config…") import_config ;;
|
||
"Tailscale ›") tailscale_menu ;;
|
||
"● "*|"○ "*) toggle_conn "$choice" ;;
|
||
esac
|
||
done
|
||
'';
|
||
in
|
||
{
|
||
config = lib.mkIf cfg.rofi.enable {
|
||
home.packages = [
|
||
nomarchy-menu
|
||
nomarchy-vpn # VPN submenu (NM connect/import + Tailscale)
|
||
pkgs.fd # files module (fuzzy search over $HOME)
|
||
pkgs.xdg-utils # xdg-open for the files + web modules
|
||
pkgs.nodejs # npx, for the Ask Claude module (claude-code from npm)
|
||
pkgs.networkmanager_dmenu # network module: rofi wifi/VPN picker
|
||
pkgs.rofi-pulse-select # audio module: sink/source switcher
|
||
# Capture › OCR region: English only — the unscoped wrapper drags
|
||
# in every language's traineddata, a real cost on a compile-from-
|
||
# source distro. More languages: override in your home.nix.
|
||
(pkgs.tesseract.override { enableLanguages = [ "eng" ]; })
|
||
];
|
||
|
||
# networkmanager_dmenu drives rofi (not bare dmenu) and uses rofi's
|
||
# active/urgent row styling for the connected/available networks, so it
|
||
# inherits the generated theme like every other menu module. Editing a
|
||
# connection drops to nmtui in the configured terminal. force: the tool
|
||
# writes a default config on first run, so an unmanaged file may already
|
||
# sit at this path — we fully own it, so overwrite rather than abort.
|
||
# compact: with False the script inserts EMPTY rows as section
|
||
# separators (eth/wifi/vpn) — blank entries in a themed picker read as
|
||
# breakage (item 22). True drops them and uses the unpadded row format,
|
||
# which also sits better in rofi's proportional font than the
|
||
# space-aligned columns. (A hidden-SSID AP still renders one nameless
|
||
# row — sec+bars only; not filterable via config, dedup keeps one.)
|
||
xdg.configFile."networkmanager-dmenu/config.ini" = {
|
||
force = true;
|
||
text = ''
|
||
[dmenu]
|
||
dmenu_command = rofi
|
||
rofi_highlight = True
|
||
compact = True
|
||
wifi_chars = ▂▄▆█
|
||
|
||
[editor]
|
||
terminal = ${cfg.terminal}
|
||
gui_if_available = False
|
||
'';
|
||
};
|
||
|
||
# The launcher theme lives beside HM's active base theme, which it
|
||
# @imports so the default launcher is byte-for-byte that theme's list.
|
||
# HM names that base by its SOURCE: a generated (attrset) theme lands as
|
||
# custom.rasi, but a whole-swap theme (a path — themes/<slug>/rofi.rasi)
|
||
# keeps its basename, rofi.rasi. Import must match, or `-theme launcher`
|
||
# dies with "custom.rasi not found" (rofi resolves the missing import
|
||
# against $HOME) on every whole-swap theme.
|
||
xdg.dataFile."rofi/themes/launcher.rasi".text =
|
||
if hasLauncherOverride
|
||
then builtins.readFile launcherOverride
|
||
else ''@import "${if hasRasiOverride then "rofi.rasi" else "custom.rasi"}"'';
|
||
|
||
programs.rofi = {
|
||
enable = true;
|
||
terminal = cfg.terminal;
|
||
font = "${t.fonts.ui} 12";
|
||
|
||
# Native rofi modi backing two menu modules: calc (live, via
|
||
# libqalculate — no qalc CLI needed) and emoji (glyph picker, copies
|
||
# via its bundled Wayland adapter). Both render through the
|
||
# generated/whole-swap theme like every other modi.
|
||
plugins = [ pkgs.rofi-calc pkgs.rofi-emoji ];
|
||
|
||
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";
|
||
# Size the window to the actual number of rows instead of always
|
||
# reserving `lines` (8) — so a 6-entry menu doesn't leave empty space,
|
||
# while the app launcher still fills + scrolls past `lines`.
|
||
fixed-num-lines = false;
|
||
# Search: case-insensitive fuzzy across every menu + the launcher, so
|
||
# "system" finds "System" and "fzr" finds "Firefox". fzf sorting ranks
|
||
# the closest match to the top (the per-keystroke modules — calc/emoji
|
||
# — opt out per-invocation with -no-sort).
|
||
matching = "fuzzy";
|
||
case-sensitive = false;
|
||
sort = true;
|
||
sorting-method = "fzf";
|
||
};
|
||
|
||
# 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/@surface are fg-derived #RRGGBBAA tints, NOT the
|
||
# palette's subtext/surface roles — "on-surface" palettes
|
||
# (flexoki-light: subtext==base, surface==text) made the
|
||
# inputbar, textbox and alternate rows invisible (item 27;
|
||
# guarded by tools/check-theme-contrast.py).
|
||
dim = mkLiteral (c.text + "80");
|
||
surface = mkLiteral (c.text + "1a");
|
||
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 4; # a little air so the rounded zebra rows separate
|
||
padding = mkLiteral "8px 0px 0px 0px";
|
||
};
|
||
|
||
# Rounded, roomy rows with subtle zebra striping: alternate rows
|
||
# lift to @surface, the selected row to @accent. normal/alternate/
|
||
# selected are rofi's row parities (the `.normal` suffix is the
|
||
# match-state, untouched by our menus).
|
||
"element" = {
|
||
padding = mkLiteral "10px 14px";
|
||
border-radius = px t.ui.rounding;
|
||
spacing = px 12; # gap between the (larger) icon and the label
|
||
};
|
||
"element normal.normal" = {
|
||
background-color = mkLiteral "@bg";
|
||
text-color = mkLiteral "@fg";
|
||
};
|
||
"element alternate.normal" = {
|
||
background-color = mkLiteral "@surface";
|
||
text-color = mkLiteral "@fg";
|
||
};
|
||
"element selected.normal" = {
|
||
background-color = mkLiteral "@accent";
|
||
text-color = mkLiteral "@bg";
|
||
};
|
||
"element-icon" = {
|
||
background-color = mkLiteral "transparent";
|
||
size = px t.ui.iconSize; # real icons (Papirus), not 1em font glyphs
|
||
};
|
||
"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";
|
||
};
|
||
};
|
||
};
|
||
};
|
||
}
|