fix: migration first-boot, fingerprint PAM, menus, mic LED
Some checks failed
Check / eval (push) Failing after 9m12s
Some checks failed
Check / eval (push) Failing after 9m12s
Post-migration annoyances from TuringMachine: - Force fprintAuth from fingerprint.pam (NixOS defaults it on with fprintd) - First-boot toast waits for swaync, retries, only marks after success - Night light status/toggle handles masked units + hyprsunset/wlsunset - VPN menu: define rofi_menu in nomarchy-vpn (was a silent no-op) - Left arrow = ↩ Back in list menus; GTK menu contrast via Stylix - Mic-mute LED udev + sync after wpctl; EasyEffects starts after tray - Migration guide: HM before GUI login, fingerprint/browser/VPN notes Verified: flake check --no-build; checks.first-boot; checks.hardware-toggles (pam=false node). V3 session items pending after user rebuild.
This commit is contained in:
@@ -226,14 +226,23 @@ Expect to resolve a few 25.11→26.05 option renames.
|
|||||||
# breaks, REBOOT and you are back in your old generation, untouched.
|
# breaks, REBOOT and you are back in your old generation, untouched.
|
||||||
sudo nixos-rebuild test --flake ~/nomarchy-migrate#default
|
sudo nixos-rebuild test --flake ~/nomarchy-migrate#default
|
||||||
|
|
||||||
# Bring the desktop (home-manager) up:
|
# Bring the desktop (home-manager) up BEFORE the first graphical login.
|
||||||
home-manager switch --flake ~/nomarchy-migrate#bernardo
|
# Hyprland without an HM generation shows the yellow "autogenerated
|
||||||
|
# config" banner (and no Nomarchy theming) — finish this step first.
|
||||||
|
home-manager switch --flake ~/nomarchy-migrate#bernardo -b bak
|
||||||
```
|
```
|
||||||
|
|
||||||
Log into Hyprland and sanity‑check: Waybar renders, `SUPER+M` opens the
|
Log into Hyprland and sanity‑check: Waybar renders, `SUPER+M` opens the
|
||||||
menu, theming is coherent, `SUPER+?` shows the cheatsheet. Confirm the
|
menu, theming is coherent, `SUPER+?` shows the cheatsheet, **no** yellow
|
||||||
machine‑specific things you care about still work — suspend/hibernate, the
|
Hyprland autogenerated banner. Confirm the machine‑specific things you
|
||||||
AMD GPU (`vainfo` → radeonsi), display brightness.
|
care about still work — suspend/hibernate, the AMD GPU (`vainfo` →
|
||||||
|
radeonsi), display brightness.
|
||||||
|
|
||||||
|
You should also get a one-shot **You're set** toast (menu / themes /
|
||||||
|
keys). If it never appears: `systemctl --user status nomarchy-first-boot`
|
||||||
|
and re-try with
|
||||||
|
`nomarchy-theme-sync set settings.firstBootShown false --no-switch`
|
||||||
|
then log out/in.
|
||||||
|
|
||||||
If anything is wrong: **reboot → old generation.** Nothing is committed as
|
If anything is wrong: **reboot → old generation.** Nothing is committed as
|
||||||
default yet.
|
default yet.
|
||||||
@@ -245,10 +254,26 @@ default yet.
|
|||||||
- **Power:** verify `powerprofilesctl get` works and the Waybar battery /
|
- **Power:** verify `powerprofilesctl get` works and the Waybar battery /
|
||||||
power‑profile icons open the power menu. Your ryzenadj scripts are gone;
|
power‑profile icons open the power menu. Your ryzenadj scripts are gone;
|
||||||
if you miss a specific TDP behaviour, that's a follow‑up, not a blocker.
|
if you miss a specific TDP behaviour, that's a follow‑up, not a blocker.
|
||||||
- **Theme:** `nomarchy-menu theme` → pick **nord** (writes
|
- **Theme:** `nomarchy-menu theme` → pick a preset (writes
|
||||||
`theme-state.json`).
|
`theme-state.json`).
|
||||||
- **Snapshots:** `nomarchy-menu` → System → Snapshots should see your
|
- **Snapshots:** `nomarchy-menu` → System → Snapshots should see your
|
||||||
existing `@snapshots` subvolume.
|
existing `@snapshots` subvolume.
|
||||||
|
- **Fingerprint:** `fingerprint.enable = true` only starts **fprintd**
|
||||||
|
(enrollment). Login/sudo finger auth is **`fingerprint.pam`** and is
|
||||||
|
opt-in — leave it commented until you've enrolled. NixOS defaults PAM
|
||||||
|
on whenever fprintd is enabled; Nomarchy forces PAM to follow the
|
||||||
|
`pam` flag, but only after a **system** rebuild. Verify with
|
||||||
|
`grep pam_fprintd /etc/pam.d/sudo` (should be empty when pam is off).
|
||||||
|
- **Browser profiles:** Nomarchy does not manage Chromium/Firefox state.
|
||||||
|
Bookmarks/extensions live under `~/.config/chromium` (or
|
||||||
|
`~/.config/google-chrome` / ungoogled paths if that was your previous
|
||||||
|
browser). A package name change can look like "extensions vanished"
|
||||||
|
while bookmarks remain — point the new browser at the old profile dir
|
||||||
|
or re-install extensions; nothing was wiped by the activation.
|
||||||
|
- **VPN:** NetworkManager connections survive under
|
||||||
|
`/etc/NetworkManager` and your home. System › VPN lists NM
|
||||||
|
`vpn`/`wireguard` profiles; import any that lived outside NM.
|
||||||
|
Tailscale is opt-in (`nomarchy.services.tailscale.enable`).
|
||||||
- **Secrets/services:** if you relied on agenix‑managed secrets for a
|
- **Secrets/services:** if you relied on agenix‑managed secrets for a
|
||||||
service, layer `agenix` back into `system.nix` as a machine‑specific
|
service, layer `agenix` back into `system.nix` as a machine‑specific
|
||||||
import (Nomarchy doesn't manage secrets). If you don't need them, leave
|
import (Nomarchy doesn't manage secrets). If you don't need them, leave
|
||||||
|
|||||||
27
flake.nix
27
flake.nix
@@ -489,15 +489,23 @@
|
|||||||
# what they SET (kernel cmdline, fprintd, PAM). Real hardware
|
# what they SET (kernel cmdline, fprintd, PAM). Real hardware
|
||||||
# behaviour (firmware/driver/device) needs bare metal and is out of
|
# behaviour (firmware/driver/device) needs bare metal and is out of
|
||||||
# scope here. Imports only hardware.nix, so the VM stays minimal.
|
# scope here. Imports only hardware.nix, so the VM stays minimal.
|
||||||
|
# Two nodes: pam=true must wire pam_fprintd; pam=false must NOT
|
||||||
|
# (NixOS defaults fprintAuth from fprintd.enable — we force off).
|
||||||
hardware-toggles = pkgs.testers.runNixOSTest {
|
hardware-toggles = pkgs.testers.runNixOSTest {
|
||||||
name = "nomarchy-hardware-toggles";
|
name = "nomarchy-hardware-toggles";
|
||||||
nodes.machine = { ... }: {
|
nodes = {
|
||||||
imports = [ ./modules/nixos/hardware.nix ];
|
machine = { ... }: {
|
||||||
nomarchy.hardware = {
|
imports = [ ./modules/nixos/hardware.nix ];
|
||||||
intel.enable = true;
|
nomarchy.hardware = {
|
||||||
amd.enable = true;
|
intel.enable = true;
|
||||||
fingerprint = { enable = true; pam = true; };
|
amd.enable = true;
|
||||||
npu.enable = true;
|
fingerprint = { enable = true; pam = true; };
|
||||||
|
npu.enable = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
nopam = { ... }: {
|
||||||
|
imports = [ ./modules/nixos/hardware.nix ];
|
||||||
|
nomarchy.hardware.fingerprint = { enable = true; pam = false; };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
testScript = ''
|
testScript = ''
|
||||||
@@ -507,6 +515,11 @@
|
|||||||
assert "i915.enable_guc=3" in cmdline, cmdline
|
assert "i915.enable_guc=3" in cmdline, cmdline
|
||||||
machine.succeed("systemctl cat fprintd.service")
|
machine.succeed("systemctl cat fprintd.service")
|
||||||
machine.succeed("grep -q pam_fprintd /etc/pam.d/sudo")
|
machine.succeed("grep -q pam_fprintd /etc/pam.d/sudo")
|
||||||
|
|
||||||
|
nopam.wait_for_unit("multi-user.target")
|
||||||
|
nopam.succeed("systemctl cat fprintd.service")
|
||||||
|
nopam.fail("grep -q pam_fprintd /etc/pam.d/sudo")
|
||||||
|
nopam.fail("grep -q pam_fprintd /etc/pam.d/login")
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,16 @@
|
|||||||
services.udiskie.enable = true;
|
services.udiskie.enable = true;
|
||||||
|
|
||||||
# Microphone noise cancellation (rnnoise) and audio EQ.
|
# Microphone noise cancellation (rnnoise) and audio EQ.
|
||||||
|
# Tray icon is built into EasyEffects 8 (Qt StatusNotifierItem); the
|
||||||
|
# daemon must start after a tray host (Waybar) or the icon is missing
|
||||||
|
# for the whole session (wwmm/easyeffects#4636).
|
||||||
services.easyeffects.enable = true;
|
services.easyeffects.enable = true;
|
||||||
|
systemd.user.services.easyeffects = {
|
||||||
|
Unit = {
|
||||||
|
After = [ "graphical-session.target" "tray.target" "waybar.service" ];
|
||||||
|
Wants = [ "tray.target" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
# Wifi from the bar: nm-applet lives in waybar's tray (SNI flag via
|
# Wifi from the bar: nm-applet lives in waybar's tray (SNI flag via
|
||||||
# preferStatusNotifierItems — without it there is no tray icon).
|
# preferStatusNotifierItems — without it there is no tray icon).
|
||||||
|
|||||||
@@ -11,8 +11,18 @@
|
|||||||
Unit = {
|
Unit = {
|
||||||
Description = "Nomarchy first-session welcome toast";
|
Description = "Nomarchy first-session welcome toast";
|
||||||
PartOf = [ "graphical-session.target" ];
|
PartOf = [ "graphical-session.target" ];
|
||||||
# After the session (and preferably the notification daemon) is up.
|
# After the session AND the notification daemon. Without
|
||||||
After = [ "graphical-session.target" "graphical-session-pre.target" ];
|
# After=swaync, first login races D-Bus Notifications and the
|
||||||
|
# toast times out (migration: journal "Timeout was reached") —
|
||||||
|
# or worse, a later success writes firstBootShown without the
|
||||||
|
# user ever seeing the toast. Wants= so we still run if swaync
|
||||||
|
# is disabled (notify-send will fail and leave the marker unset).
|
||||||
|
After = [
|
||||||
|
"graphical-session.target"
|
||||||
|
"graphical-session-pre.target"
|
||||||
|
"swaync.service"
|
||||||
|
];
|
||||||
|
Wants = [ "swaync.service" ];
|
||||||
};
|
};
|
||||||
Service = {
|
Service = {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
@@ -27,8 +37,8 @@
|
|||||||
]}"
|
]}"
|
||||||
"NOMARCHY_PATH=%h/.nomarchy"
|
"NOMARCHY_PATH=%h/.nomarchy"
|
||||||
];
|
];
|
||||||
# Brief settle so swaync is accepting toasts (live ISO uses sleep 3).
|
# Brief settle after swaync is up; the script also retries.
|
||||||
ExecStartPre = "${pkgs.coreutils}/bin/sleep 2";
|
ExecStartPre = "${pkgs.coreutils}/bin/sleep 1";
|
||||||
ExecStart = "${pkgs.nomarchy-first-boot}/bin/nomarchy-first-boot";
|
ExecStart = "${pkgs.nomarchy-first-boot}/bin/nomarchy-first-boot";
|
||||||
};
|
};
|
||||||
Install.WantedBy = [ "graphical-session.target" ];
|
Install.WantedBy = [ "graphical-session.target" ];
|
||||||
|
|||||||
@@ -20,14 +20,29 @@ let
|
|||||||
# --custom-progress is the mic level (0.0-1.0), --custom-icon the
|
# --custom-progress is the mic level (0.0-1.0), --custom-icon the
|
||||||
# muted/active glyph. Pure bash builtins for parsing so the script needs no
|
# muted/active glyph. Pure bash builtins for parsing so the script needs no
|
||||||
# PATH beyond the two pinned tools.
|
# PATH beyond the two pinned tools.
|
||||||
|
#
|
||||||
|
# Hardware mic-mute LEDs (ThinkPad platform::micmute, HDA *::micmute, …)
|
||||||
|
# are driven by the kernel's audio-micmute trigger, which tracks ALSA —
|
||||||
|
# not PipeWire software mute. Sync the LED from the resulting mute state
|
||||||
|
# when the node is group-writable (udev rule in modules/nixos/default.nix).
|
||||||
micMute = pkgs.writeShellScript "nomarchy-mic-mute" ''
|
micMute = pkgs.writeShellScript "nomarchy-mic-mute" ''
|
||||||
${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_SOURCE@ toggle
|
${pkgs.wireplumber}/bin/wpctl set-mute @DEFAULT_SOURCE@ toggle
|
||||||
out=$(${pkgs.wireplumber}/bin/wpctl get-volume @DEFAULT_SOURCE@)
|
out=$(${pkgs.wireplumber}/bin/wpctl get-volume @DEFAULT_SOURCE@)
|
||||||
set -- $out # "Volume: 0.85 [MUTED]" -> $2 is the level
|
set -- $out # "Volume: 0.85 [MUTED]" -> $2 is the level
|
||||||
case "$out" in
|
case "$out" in
|
||||||
*MUTED*) icon=microphone-sensitivity-muted-symbolic ;;
|
*MUTED*) icon=microphone-sensitivity-muted-symbolic; led=1 ;;
|
||||||
*) icon=microphone-sensitivity-high-symbolic ;;
|
*) icon=microphone-sensitivity-high-symbolic; led=0 ;;
|
||||||
esac
|
esac
|
||||||
|
for led_dir in /sys/class/leds/*micmute* /sys/class/leds/*::micmute; do
|
||||||
|
[ -e "$led_dir/brightness" ] || continue
|
||||||
|
if [ -w "$led_dir/brightness" ]; then
|
||||||
|
# Drop the kernel trigger so a stuck audio-micmute state cannot
|
||||||
|
# override our write (verified: ALSA Capture mute alone does not
|
||||||
|
# flip platform::micmute on ThinkPad T14s).
|
||||||
|
[ -w "$led_dir/trigger" ] && echo none > "$led_dir/trigger" 2>/dev/null || true
|
||||||
|
echo "$led" > "$led_dir/brightness" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
${pkgs.swayosd}/bin/swayosd-client --custom-icon "$icon" --custom-progress "$2"
|
${pkgs.swayosd}/bin/swayosd-client --custom-icon "$icon" --custom-progress "$2"
|
||||||
'';
|
'';
|
||||||
|
|
||||||
@@ -446,6 +461,10 @@ in
|
|||||||
];
|
];
|
||||||
|
|
||||||
misc = {
|
misc = {
|
||||||
|
# Kill the water-drop splash + the yellow "autogenerated config"
|
||||||
|
# banner. Migrations that log into Hyprland before the first
|
||||||
|
# home-manager switch still see the banner (no managed conf yet) —
|
||||||
|
# see docs/MIGRATION.md. Once HM owns hyprland.conf these stay off.
|
||||||
disable_hyprland_logo = lib.mkDefault true;
|
disable_hyprland_logo = lib.mkDefault true;
|
||||||
disable_splash_rendering = lib.mkDefault true;
|
disable_splash_rendering = lib.mkDefault true;
|
||||||
force_default_wallpaper = lib.mkDefault 0;
|
force_default_wallpaper = lib.mkDefault 0;
|
||||||
|
|||||||
@@ -45,29 +45,42 @@ let
|
|||||||
v=$(${sync} get settings.nightlight.on 2>/dev/null) || v=${onDefault}
|
v=$(${sync} get settings.nightlight.on 2>/dev/null) || v=${onDefault}
|
||||||
case "$v" in true|True) return 0 ;; *) return 1 ;; esac
|
case "$v" in true|True) return 0 ;; *) return 1 ;; esac
|
||||||
}
|
}
|
||||||
installed() { systemctl --user cat "$unit" >/dev/null 2>&1; }
|
# LoadState=loaded means a real unit (not the empty-file mask HM/packages
|
||||||
start() { systemctl --user start "$unit" 2>/dev/null || true; }
|
# leave behind for unused hyprsunset). `systemctl cat` alone succeeds on
|
||||||
|
# a masked unit and wrongly reported "installed" → silent no-op start.
|
||||||
|
installed() {
|
||||||
|
[ "$(systemctl --user show -p LoadState --value "$unit" 2>/dev/null)" = loaded ]
|
||||||
|
}
|
||||||
|
active() { systemctl --user is-active --quiet "$unit" 2>/dev/null; }
|
||||||
|
start() {
|
||||||
|
systemctl --user daemon-reload 2>/dev/null || true
|
||||||
|
systemctl --user unmask "$unit" 2>/dev/null || true
|
||||||
|
systemctl --user start "$unit" 2>/dev/null || true
|
||||||
|
}
|
||||||
stop() { systemctl --user stop "$unit" 2>/dev/null || true; }
|
stop() { systemctl --user stop "$unit" 2>/dev/null || true; }
|
||||||
case "''${1:-toggle}" in
|
case "''${1:-toggle}" in
|
||||||
should-start) is_on ;; # ExecCondition gate (login/reboot)
|
should-start) is_on ;; # ExecCondition gate (login/reboot)
|
||||||
status)
|
status)
|
||||||
# Waybar (polls every 3s): moon while running; print nothing otherwise so
|
# Waybar (polls every 3s): moon while running; print nothing otherwise so
|
||||||
# the module self-hides — enable / re-enable from the System menu.
|
# the module self-hides — enable / re-enable from Look & Feel.
|
||||||
systemctl --user is-active --quiet "$unit" \
|
# `active` alone is the truth for the moon; menu labels use the same.
|
||||||
|
active \
|
||||||
&& printf '{"text":"","tooltip":"Night light on — ${if geo then "follows your location" else "warm on schedule"} (click to disable)","class":"on"}\n'
|
&& printf '{"text":"","tooltip":"Night light on — ${if geo then "follows your location" else "warm on schedule"} (click to disable)","class":"on"}\n'
|
||||||
exit 0 ;;
|
exit 0 ;;
|
||||||
|
# Plain on/off string for menus (hyprsunset OR wlsunset; not hard-coded).
|
||||||
|
is-active) if active; then echo on; else echo off; fi ;;
|
||||||
on)
|
on)
|
||||||
if installed; then write_on true; start; else install_feature; start; fi ;;
|
if installed; then write_on true; start; else install_feature; start; fi ;;
|
||||||
off) write_on false; stop ;;
|
off) write_on false; stop ;;
|
||||||
toggle)
|
toggle)
|
||||||
if systemctl --user is-active --quiet "$unit"; then
|
if active; then
|
||||||
write_on false; stop # on -> off (instant)
|
write_on false; stop # on -> off (instant)
|
||||||
elif installed; then
|
elif installed; then
|
||||||
write_on true; start # installed, off -> on (instant)
|
write_on true; start # installed, off -> on (instant)
|
||||||
else
|
else
|
||||||
install_feature; start # first enable (rebuilds)
|
install_feature; start # first enable (rebuilds)
|
||||||
fi ;;
|
fi ;;
|
||||||
*) echo "usage: nomarchy-nightlight [toggle|status|on|off|should-start]" >&2; exit 64 ;;
|
*) echo "usage: nomarchy-nightlight [toggle|status|on|off|should-start|is-active]" >&2; exit 64 ;;
|
||||||
esac
|
esac
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
|
|||||||
@@ -234,7 +234,34 @@ let
|
|||||||
# when config.rasi has case-sensitive: false for drun/modi — so
|
# when config.rasi has case-sensitive: false for drun/modi — so
|
||||||
# SUPER+Space (apps) matched "ste"→Steam while the main Menu filter
|
# SUPER+Space (apps) matched "ste"→Steam while the main Menu filter
|
||||||
# did not match "sys"→System. Always pass -i on menu pickers.
|
# did not match "sys"→System. Always pass -i on menu pickers.
|
||||||
rofi_menu() { command rofi -dmenu -i "$@"; }
|
#
|
||||||
|
# Left arrow → one level up (same as choosing "↩ Back"). rofi's
|
||||||
|
# -kb-custom-1 returns exit 10; Esc still returns non-zero and quits
|
||||||
|
# the whole menu. Free-text prompts should treat "$BACK" as cancel.
|
||||||
|
# Pass --grid as the first arg to skip Left=Back (multi-column theme
|
||||||
|
# picker needs Left for column motion).
|
||||||
|
rofi_menu() {
|
||||||
|
local out rc left_back=1
|
||||||
|
if [ "''${1:-}" = --grid ]; then
|
||||||
|
left_back=0
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
if [ "$left_back" -eq 1 ]; then
|
||||||
|
out=$(command rofi -dmenu -i -kb-custom-1 Left "$@" </dev/stdin)
|
||||||
|
else
|
||||||
|
out=$(command rofi -dmenu -i "$@" </dev/stdin)
|
||||||
|
fi
|
||||||
|
rc=$?
|
||||||
|
if [ "$left_back" -eq 1 ] && [ "$rc" -eq 10 ]; then
|
||||||
|
printf '%s\n' "$BACK"
|
||||||
|
return 0
|
||||||
|
elif [ "$rc" -eq 0 ]; then
|
||||||
|
printf '%s\n' "$out"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
case "''${1:-}" in
|
case "''${1:-}" in
|
||||||
power)
|
power)
|
||||||
@@ -395,7 +422,7 @@ ${themeSlugMap}
|
|||||||
choice=$( {
|
choice=$( {
|
||||||
${themeRows}
|
${themeRows}
|
||||||
back
|
back
|
||||||
} | rofi_menu -show-icons -p Theme -theme-str ${themeGridThemeStr}) || exit 0
|
} | rofi_menu --grid -show-icons -p Theme -theme-str ${themeGridThemeStr}) || exit 0
|
||||||
[ "$choice" = "$BACK" ] && exec "$0"
|
[ "$choice" = "$BACK" ] && exec "$0"
|
||||||
choice="''${choice% ✓}" # drop the active marker if present
|
choice="''${choice% ✓}" # drop the active marker if present
|
||||||
slug="''${THEME_SLUG[$choice]:-}"
|
slug="''${THEME_SLUG[$choice]:-}"
|
||||||
@@ -429,7 +456,7 @@ ${themeRows}
|
|||||||
|
|
||||||
web)
|
web)
|
||||||
q=$(rofi_menu -p search < /dev/null) || exit 0
|
q=$(rofi_menu -p search < /dev/null) || exit 0
|
||||||
[ -n "$q" ] || exit 0
|
{ [ -n "$q" ] && [ "$q" != "$BACK" ]; } || exit 0
|
||||||
exec xdg-open "https://www.google.com/search?q=$(urlencode "$q")" ;;
|
exec xdg-open "https://www.google.com/search?q=$(urlencode "$q")" ;;
|
||||||
|
|
||||||
network)
|
network)
|
||||||
@@ -599,7 +626,10 @@ ${themeRows}
|
|||||||
row "Theme${menuHint "theme"}" preferences-desktop-theme
|
row "Theme${menuHint "theme"}" preferences-desktop-theme
|
||||||
row "Next wallpaper${hintForAction "exec, nomarchy-theme-sync bg next"}" preferences-desktop-wallpaper
|
row "Next wallpaper${hintForAction "exec, nomarchy-theme-sync bg next"}" preferences-desktop-wallpaper
|
||||||
row "Reset wallpaper (auto)" preferences-desktop-wallpaper
|
row "Reset wallpaper (auto)" preferences-desktop-wallpaper
|
||||||
if systemctl --user is-active --quiet hyprsunset.service 2>/dev/null
|
# Use the toggle script (knows hyprsunset vs wlsunset); a hard-coded
|
||||||
|
# hyprsunset-only check always showed (off) in geo mode and when
|
||||||
|
# the unit was still the empty HM mask from a disabled package unit.
|
||||||
|
if [ "$(nomarchy-nightlight is-active 2>/dev/null)" = on ]
|
||||||
then row "Night light (on)" weather-clear-night
|
then row "Night light (on)" weather-clear-night
|
||||||
else row "Night light (off)" weather-clear-night
|
else row "Night light (off)" weather-clear-night
|
||||||
fi
|
fi
|
||||||
@@ -680,7 +710,7 @@ ${themeRows}
|
|||||||
# which lags behind model releases; npx caches after first run. The
|
# which lags behind model releases; npx caches after first run. The
|
||||||
# REPL stays open for follow-ups.
|
# REPL stays open for follow-ups.
|
||||||
q=$(rofi_menu -p claude < /dev/null) || exit 0
|
q=$(rofi_menu -p claude < /dev/null) || exit 0
|
||||||
[ -n "$q" ] || exit 0
|
{ [ -n "$q" ] && [ "$q" != "$BACK" ]; } || exit 0
|
||||||
exec ${cfg.terminal} -e npx --yes @anthropic-ai/claude-code@latest "$q" ;;
|
exec ${cfg.terminal} -e npx --yes @anthropic-ai/claude-code@latest "$q" ;;
|
||||||
|
|
||||||
dnd)
|
dnd)
|
||||||
@@ -752,9 +782,9 @@ ${themeRows}
|
|||||||
case "$choice" in "Sunset"*) key=sunset; label=Sunset; cur="''${sunset:-20:00}" ;; esac
|
case "$choice" in "Sunset"*) key=sunset; label=Sunset; cur="''${sunset:-20:00}" ;; esac
|
||||||
t=$(rofi_menu -p "$label HH:MM (now $cur)" < /dev/null) || exec "$0" autotheme
|
t=$(rofi_menu -p "$label HH:MM (now $cur)" < /dev/null) || exec "$0" autotheme
|
||||||
case "$t" in
|
case "$t" in
|
||||||
|
""|"$BACK") : ;;
|
||||||
[01][0-9]:[0-5][0-9]|2[0-3]:[0-5][0-9])
|
[01][0-9]:[0-5][0-9]|2[0-3]:[0-5][0-9])
|
||||||
nomarchy-theme-sync --quiet set "settings.autoTheme.$key" "$t" --no-switch ;;
|
nomarchy-theme-sync --quiet set "settings.autoTheme.$key" "$t" --no-switch ;;
|
||||||
"") : ;;
|
|
||||||
*) notify-send "Auto theme" "Ignored '$t' — use HH:MM (24-hour)." ;;
|
*) notify-send "Auto theme" "Ignored '$t' — use HH:MM (24-hour)." ;;
|
||||||
esac
|
esac
|
||||||
exec "$0" autotheme ;;
|
exec "$0" autotheme ;;
|
||||||
@@ -1119,7 +1149,7 @@ ${themeRows}
|
|||||||
*60%*) set_limit 60 "Set to 60%" ;;
|
*60%*) set_limit 60 "Set to 60%" ;;
|
||||||
*Custom*)
|
*Custom*)
|
||||||
limit=$(rofi_menu -p "Charge limit % (50–100)" < /dev/null) || exit 0
|
limit=$(rofi_menu -p "Charge limit % (50–100)" < /dev/null) || exit 0
|
||||||
[ -n "$limit" ] || exit 0
|
{ [ -n "$limit" ] && [ "$limit" != "$BACK" ]; } || exit 0
|
||||||
case "$limit" in
|
case "$limit" in
|
||||||
*[!0-9]*) notify-send "Battery limit" "Not a whole number: $limit"; exit 0 ;;
|
*[!0-9]*) notify-send "Battery limit" "Not a whole number: $limit"; exit 0 ;;
|
||||||
esac
|
esac
|
||||||
@@ -1172,6 +1202,23 @@ ${themeRows}
|
|||||||
nomarchy-vpn = pkgs.writeShellScriptBin "nomarchy-vpn" ''
|
nomarchy-vpn = pkgs.writeShellScriptBin "nomarchy-vpn" ''
|
||||||
set -u
|
set -u
|
||||||
BACK="↩ Back"
|
BACK="↩ Back"
|
||||||
|
# Own helper — this is a separate package from nomarchy-menu, so the
|
||||||
|
# menu's rofi_menu function is not in scope (was a silent no-op: every
|
||||||
|
# VPN submenu died with "rofi_menu: command not found").
|
||||||
|
rofi_menu() {
|
||||||
|
local out rc
|
||||||
|
out=$(command rofi -dmenu -i -kb-custom-1 Left "$@" </dev/stdin)
|
||||||
|
rc=$?
|
||||||
|
if [ "$rc" -eq 10 ]; then
|
||||||
|
printf '%s\n' "$BACK"
|
||||||
|
return 0
|
||||||
|
elif [ "$rc" -eq 0 ]; then
|
||||||
|
printf '%s\n' "$out"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
conn_rows() {
|
conn_rows() {
|
||||||
nmcli -t -f NAME,TYPE,STATE connection show 2>/dev/null \
|
nmcli -t -f NAME,TYPE,STATE connection show 2>/dev/null \
|
||||||
@@ -1180,12 +1227,15 @@ ${themeRows}
|
|||||||
|
|
||||||
toggle_conn() {
|
toggle_conn() {
|
||||||
mark=''${1%% *}; name=''${1#* }
|
mark=''${1%% *}; name=''${1#* }
|
||||||
|
err=
|
||||||
if [ "$mark" = "●" ]; then
|
if [ "$mark" = "●" ]; then
|
||||||
nmcli connection down "$name" >/dev/null 2>&1 \
|
err=$(nmcli connection down "$name" 2>&1) \
|
||||||
&& notify-send "VPN" "Disconnected $name" || notify-send "VPN" "Could not disconnect $name"
|
&& notify-send "VPN" "Disconnected $name" \
|
||||||
|
|| notify-send "VPN" "Could not disconnect $name''${err:+ — $err}"
|
||||||
else
|
else
|
||||||
nmcli connection up "$name" >/dev/null 2>&1 \
|
err=$(nmcli connection up "$name" 2>&1) \
|
||||||
&& notify-send "VPN" "Connected $name" || notify-send "VPN" "Could not connect $name (auth or bad config?)"
|
&& notify-send "VPN" "Connected $name" \
|
||||||
|
|| notify-send "VPN" "Could not connect $name''${err:+ — $err}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1196,11 +1246,9 @@ ${themeRows}
|
|||||||
| rofi_menu -p "Import VPN config" ) || return
|
| rofi_menu -p "Import VPN config" ) || return
|
||||||
{ [ -z "$file" ] || [ "$file" = "$BACK" ]; } && return
|
{ [ -z "$file" ] || [ "$file" = "$BACK" ]; } && return
|
||||||
case "$file" in *.ovpn) type=openvpn ;; *) type=wireguard ;; esac
|
case "$file" in *.ovpn) type=openvpn ;; *) type=wireguard ;; esac
|
||||||
if nmcli connection import type "$type" file "$HOME/$file" >/dev/null 2>&1; then
|
err=$(nmcli connection import type "$type" file "$HOME/$file" 2>&1) \
|
||||||
notify-send "VPN" "Imported $type config: $(basename "$file")"
|
&& notify-send "VPN" "Imported $type config: $(basename "$file")" \
|
||||||
else
|
|| notify-send "VPN" "Import failed''${err:+ — $err}"
|
||||||
notify-send "VPN" "Import failed ($type plugin missing, or bad file?)"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ts_state() { tailscale status --json 2>/dev/null | jq -r '.BackendState // "Unknown"'; }
|
ts_state() { tailscale status --json 2>/dev/null | jq -r '.BackendState // "Unknown"'; }
|
||||||
@@ -1257,14 +1305,18 @@ ${themeRows}
|
|||||||
while :; do
|
while :; do
|
||||||
have_ts=""
|
have_ts=""
|
||||||
command -v tailscale >/dev/null 2>&1 && have_ts=1
|
command -v tailscale >/dev/null 2>&1 && have_ts=1
|
||||||
|
rows=$(conn_rows)
|
||||||
choice=$( {
|
choice=$( {
|
||||||
conn_rows
|
if [ -n "$rows" ]; then printf '%s\n' "$rows"
|
||||||
|
else printf 'No VPNs yet — import a config or enable Tailscale\n'
|
||||||
|
fi
|
||||||
printf 'Import config…\n'
|
printf 'Import config…\n'
|
||||||
[ -n "$have_ts" ] && printf 'Tailscale ›\n'
|
[ -n "$have_ts" ] && printf 'Tailscale ›\n'
|
||||||
printf '%s\n' "$BACK"
|
printf '%s\n' "$BACK"
|
||||||
} | rofi_menu -p VPN ) || exit 0
|
} | rofi_menu -p VPN ) || exit 0
|
||||||
case "$choice" in
|
case "$choice" in
|
||||||
"$BACK"|"") exec nomarchy-menu system ;;
|
"$BACK"|"") exec nomarchy-menu system ;;
|
||||||
|
"No VPNs yet"*) ;; # hint row — stay in menu
|
||||||
"Import config…") import_config ;;
|
"Import config…") import_config ;;
|
||||||
"Tailscale ›") tailscale_menu ;;
|
"Tailscale ›") tailscale_menu ;;
|
||||||
"● "*|"○ "*) toggle_conn "$choice" ;;
|
"● "*|"○ "*) toggle_conn "$choice" ;;
|
||||||
|
|||||||
@@ -45,7 +45,56 @@ in
|
|||||||
base16Scheme = base16;
|
base16Scheme = base16;
|
||||||
|
|
||||||
targets = {
|
targets = {
|
||||||
gtk.enable = true;
|
gtk = {
|
||||||
|
enable = true;
|
||||||
|
# nm-applet / classic GTK menus: Stylix base16 selection can
|
||||||
|
# collapse into surface/muted on dark themes (Boreal: selected
|
||||||
|
# row and submenu arrows were effectively invisible). High-
|
||||||
|
# contrast selected rows + explicit arrow colour. Appended to
|
||||||
|
# Stylix's generated gtk.css (not gtk.gtk3.extraCss — that is a
|
||||||
|
# no-op under Stylix).
|
||||||
|
extraCss = ''
|
||||||
|
menu menuitem, .menu menuitem, .context-menu menuitem {
|
||||||
|
color: #${base16.base05};
|
||||||
|
}
|
||||||
|
menu menuitem:hover, menu menuitem:selected,
|
||||||
|
.menu menuitem:hover, .menu menuitem:selected,
|
||||||
|
.context-menu menuitem:hover, .context-menu menuitem:selected,
|
||||||
|
menubar > menuitem:hover {
|
||||||
|
background-color: #${base16.base0D};
|
||||||
|
color: #${base16.base00};
|
||||||
|
}
|
||||||
|
menu menuitem:disabled, .menu menuitem:disabled {
|
||||||
|
color: #${base16.base03};
|
||||||
|
}
|
||||||
|
/* Submenu disclosure arrows — without this they pick muted. */
|
||||||
|
menu menuitem arrow, .menu menuitem arrow,
|
||||||
|
menu menuitem:hover arrow, menu menuitem:selected arrow {
|
||||||
|
min-width: 16px;
|
||||||
|
min-height: 16px;
|
||||||
|
margin-left: 8px;
|
||||||
|
color: inherit;
|
||||||
|
-gtk-icon-source: -gtk-icontheme("pan-end-symbolic");
|
||||||
|
}
|
||||||
|
menu menuitem:hover arrow, menu menuitem:selected arrow {
|
||||||
|
color: #${base16.base00};
|
||||||
|
}
|
||||||
|
check, radio {
|
||||||
|
min-width: 14px;
|
||||||
|
min-height: 14px;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
/* GTK4 / popover menus (some portals). */
|
||||||
|
popover.menu contents modelbutton {
|
||||||
|
color: #${base16.base05};
|
||||||
|
}
|
||||||
|
popover.menu contents modelbutton:hover,
|
||||||
|
popover.menu contents modelbutton:selected {
|
||||||
|
background-color: #${base16.base0D};
|
||||||
|
color: #${base16.base00};
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
qt.enable = true;
|
qt.enable = true;
|
||||||
# No-op unless programs.zathura is on (viewers.nix enables it).
|
# No-op unless programs.zathura is on (viewers.nix enables it).
|
||||||
zathura.enable = true;
|
zathura.enable = true;
|
||||||
|
|||||||
@@ -450,6 +450,17 @@ in
|
|||||||
# users without root. Harmless baseline even if the OSD is disabled.
|
# users without root. Harmless baseline even if the OSD is disabled.
|
||||||
services.udev.packages = [ pkgs.swayosd ];
|
services.udev.packages = [ pkgs.swayosd ];
|
||||||
|
|
||||||
|
# Mic-mute LED sysfs nodes (ThinkPad platform::micmute, HDA *::micmute)
|
||||||
|
# default to root-only. PipeWire mute does not drive the kernel's
|
||||||
|
# audio-micmute trigger, so nomarchy-mic-mute writes brightness itself.
|
||||||
|
# Group `video` matches the backlight udev pattern (swayosd) and the
|
||||||
|
# template login user's extraGroups.
|
||||||
|
services.udev.extraRules = lib.mkAfter ''
|
||||||
|
ACTION=="add", SUBSYSTEM=="leds", KERNEL=="*micmute*", \
|
||||||
|
RUN+="${pkgs.coreutils}/bin/chgrp video /sys/class/leds/%k/brightness /sys/class/leds/%k/trigger", \
|
||||||
|
RUN+="${pkgs.coreutils}/bin/chmod g+w /sys/class/leds/%k/brightness /sys/class/leds/%k/trigger"
|
||||||
|
'';
|
||||||
|
|
||||||
# ── Nix itself ───────────────────────────────────────────────────
|
# ── Nix itself ───────────────────────────────────────────────────
|
||||||
nix = {
|
nix = {
|
||||||
settings = {
|
settings = {
|
||||||
|
|||||||
@@ -193,12 +193,19 @@ in
|
|||||||
})
|
})
|
||||||
|
|
||||||
# ── Fingerprint ────────────────────────────────────────────────────
|
# ── Fingerprint ────────────────────────────────────────────────────
|
||||||
|
# NixOS defaults security.pam.services.*.fprintAuth to
|
||||||
|
# services.fprintd.enable — so turning on fprintd alone would enable
|
||||||
|
# finger auth for login/sudo/greetd/passwd/… even when the user (or
|
||||||
|
# the migration template) left fingerprint.pam commented off. Force
|
||||||
|
# every interactive service we care about to follow our opt-in flag.
|
||||||
(lib.mkIf cfg.fingerprint.enable {
|
(lib.mkIf cfg.fingerprint.enable {
|
||||||
services.fprintd.enable = true;
|
services.fprintd.enable = true;
|
||||||
})
|
security.pam.services = lib.genAttrs [
|
||||||
(lib.mkIf (cfg.fingerprint.enable && cfg.fingerprint.pam) {
|
"login" "sudo" "su" "greetd" "hyprlock" "sshd"
|
||||||
security.pam.services.login.fprintAuth = true;
|
"passwd" "chsh" "chfn" "chpasswd"
|
||||||
security.pam.services.sudo.fprintAuth = true;
|
"polkit-1" "swaylock"
|
||||||
|
"groupadd" "groupdel" "groupmod" "groupmems"
|
||||||
|
] (_: { fprintAuth = cfg.fingerprint.pam; });
|
||||||
})
|
})
|
||||||
|
|
||||||
# ── Newest kernel for very-new hardware (opt-in escape hatch) ──────
|
# ── Newest kernel for very-new hardware (opt-in escape hatch) ──────
|
||||||
|
|||||||
@@ -19,13 +19,32 @@ case "$shown" in
|
|||||||
true|1|yes) exit 0 ;;
|
true|1|yes) exit 0 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Dismissible sheet, not a wizard. Body lines match SUPER+? mnemonics.
|
# Wait for the notification daemon (swaync). On first login the unit can
|
||||||
notify-send -a Nomarchy -u normal -t 0 \
|
# race graphical-session and get "Timeout was reached" from D-Bus — then
|
||||||
"You're set" \
|
# either no toast, or a toast that never lands while we still write the
|
||||||
"SUPER+M menu · SUPER+T themes · SUPER+? keys
|
# marker. Retry notify-send; only persist the marker after a success.
|
||||||
|
body="SUPER+M menu · SUPER+T themes · SUPER+? keys
|
||||||
Wi‑Fi: System › Network (or the bar tray)
|
Wi‑Fi: System › Network (or the bar tray)
|
||||||
Anything off? System › Doctor"
|
Anything off? System › Doctor"
|
||||||
|
|
||||||
|
ok=
|
||||||
|
i=0
|
||||||
|
while [ "$i" -lt 8 ]; do
|
||||||
|
if notify-send -a Nomarchy -u normal -t 0 \
|
||||||
|
"You're set" \
|
||||||
|
"$body"; then
|
||||||
|
ok=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
i=$((i + 1))
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$ok" ]; then
|
||||||
|
# Leave firstBootShown alone so the next login can try again.
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Persist in the checkout so re-login is silent. --no-switch: marker only.
|
# Persist in the checkout so re-login is silent. --no-switch: marker only.
|
||||||
if ! nomarchy-theme-sync --quiet set settings.firstBootShown true --no-switch; then
|
if ! nomarchy-theme-sync --quiet set settings.firstBootShown true --no-switch; then
|
||||||
# No writable flake checkout (or tool missing) — still showed the toast;
|
# No writable flake checkout (or tool missing) — still showed the toast;
|
||||||
|
|||||||
Reference in New Issue
Block a user