Compare commits

..
16 Commits
Author SHA1 Message Date
Black_Raven 8d830eab1e feat(Thinkpad): Added bottles to thinkpad 2026-11-01 15:41:09 +03:00
Black_Raven 9f371b6ab1 fix(Thinkpad): Remove gamemode 2026-11-01 15:39:07 +03:00
Black_Raven 2666a84516 feat(hyperlock): Added enabling hyperlock after sleep mode 2026-11-01 15:39:07 +03:00
Black_Raven 6d699a2cfa feat(user): Remov zed idea 2026-10-26 14:33:50 +03:00
Black_Raven 66714d0675 feat(sops): Added secrets for nix customization 2026-10-21 21:32:57 +03:00
Black_Raven 33956f2edf feat(helix): Added helix customization 2026-10-21 21:32:57 +03:00
Black_Raven 03160f1c60 Update 21.10.26 2026-10-21 21:08:29 +03:00
Black_Raven c2599707b6 fix(modules): Delete unused packages 2026-10-21 11:30:19 +03:00
Black_Raven e2d2ce6c6c feat(fprint): update fprint to new device 2026-10-21 11:29:54 +03:00
Black_Raven b3913c9c9b feat(kirill): Wallpaper_changer rewrite to new keys 2026-10-17 17:19:44 +03:00
Black_Raven 9d9c429033 feat(sops): Restructuring of sops keys 2026-10-17 17:17:23 +03:00
Black_Raven e1672d7cbe feat(wallpaper_changer): Added listing and history 2026-10-16 21:44:08 +03:00
Black_Raven 9e44cb4c99 feat(ThinkPad): Added sops and 1-st key
Also have an instruktion for sops
2026-10-16 21:43:45 +03:00
Black_Raven d7a7627163 feat(ThinkPad): Add aliases for charge 2026-10-15 21:43:56 +03:00
Black_Raven dd37b43078 fix(thinkpad): fix batery config for new laptop 2026-10-15 20:13:41 +03:00
Black_Raven ecebf8d98c fix(wallpaper_changer): revrite wallpaper changer to user Hyprland conf and add walls from wallhaven 2026-10-15 20:12:46 +03:00
18 changed files with 1168 additions and 84 deletions

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
keys:
- &key age1tx6jdr6535xxeygpwkkgvdwc24tulr420cm7gk56xzfp0slzeqks2nwpyg
creation_rules:
- path_regex: host/[^/]+/secrets/.+$
key_groups:
- age:
- *key
- path_regex: user/[^/]+/secrets\.yaml$
key_groups:
- age:
- *key
Generated
+93 -49
View File
@@ -80,11 +80,11 @@
"tg_owt": "tg_owt"
},
"locked": {
"lastModified": 1790620039,
"narHash": "sha256-vFY8jKgUupRGrcO9ZfVyVNwOLxRa7dqitXYwDUQqUaU=",
"lastModified": 1792306878,
"narHash": "sha256-x9gB1qSAWKDKiX3uW0Z5asE2XzxkCfdVk5B/Og+fDlo=",
"owner": "ayugram-port",
"repo": "ayugram-desktop",
"rev": "3015ae5439dd2c14f15c76c3d451610456fa140d",
"rev": "5aafef7f62261f9dacb4e95aed950a4cc379273e",
"type": "github"
},
"original": {
@@ -268,6 +268,28 @@
"type": "gitlab"
}
},
"helix-steel": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1792336814,
"narHash": "sha256-FU4WIMHhIyvEMxHeN8+Qgb3VHsU6DP74ylsyZACLzM8=",
"owner": "mattwparas",
"repo": "helix",
"rev": "16a26934f39152b5cd81bf2f6944e18e680314d0",
"type": "github"
},
"original": {
"owner": "mattwparas",
"ref": "steel-event-system",
"repo": "helix",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
@@ -275,11 +297,11 @@
]
},
"locked": {
"lastModified": 1791569574,
"narHash": "sha256-5sViLmjhTCtBTvhjSXXvd5LDrAFMydFFKZQisjE8Vds=",
"lastModified": 1792495086,
"narHash": "sha256-TyoYH983Nsc++1scw0SoOEKshPSJWJDxK7tTmuZ974M=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "6b88c12cc6d234de4888f5d21076fb11199d0844",
"rev": "ec80131710bffcdf3b8498cf41f3a0579833b023",
"type": "github"
},
"original": {
@@ -440,11 +462,11 @@
]
},
"locked": {
"lastModified": 1791378645,
"narHash": "sha256-9Eix+ZUcI9IUklrtJAD9l/nTcS/Byz7fi94Y3InxS2Y=",
"lastModified": 1792508453,
"narHash": "sha256-y+V5RFJcjnV1+ZzImkfiSJ97iDIflanjP2OR9lemvTs=",
"owner": "hyprwm",
"repo": "hyprland-plugins",
"rev": "428bfcd1c9ee34965400bb359349f518140c9920",
"rev": "d2427d4d15b36df40468f5a58bbd67cdba19835d",
"type": "github"
},
"original": {
@@ -672,11 +694,11 @@
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1791426274,
"narHash": "sha256-MWhktfFtFE8Jl7bf9Pkp7TR5MSdYCjCswSo0Gkb4ywc=",
"lastModified": 1792503283,
"narHash": "sha256-vhe2jyvRCgufWPqDuDq1S3t3ypE+ka46IFutV02xKso=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "7c8764b7c7b09b34f632464276218ef9090eaa11",
"rev": "cd4ea8d57de16f4810bc5f14207bac45fbf19f67",
"type": "github"
},
"original": {
@@ -688,11 +710,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1790463110,
"narHash": "sha256-hKlVl12B1dF0Q5vd9dY3lIJM5mFGWYSlXwSLAqHZ1+s=",
"lastModified": 1792094642,
"narHash": "sha256-zp8ToDMnqZcu4o4IH9pdwXFXGp7yLm/M0dEnNWnyd7I=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "e158d9ed9b51c98974c5e66e1ba1c9e0255fecaa",
"rev": "fe13242281a67641c0102cdb1087f0ebe586c7df",
"type": "github"
},
"original": {
@@ -720,11 +742,11 @@
},
"nixpkgs_4": {
"locked": {
"lastModified": 1791456040,
"narHash": "sha256-H83yz/do+hjOO0uujHyH8RgtIQP2v7Mw+cLwcZK/xKc=",
"lastModified": 1792502884,
"narHash": "sha256-IoNisKcGbUjdd45qCqywGMCYmZ5EnkxezvLII0FWwNA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "e7439b6b14ad3cc35d05608ebca9bce01a25f5f8",
"rev": "02c087f3fe673c9e317f206a627cb6b910336dbe",
"type": "github"
},
"original": {
@@ -736,11 +758,11 @@
},
"nixpkgs_5": {
"locked": {
"lastModified": 1790769526,
"narHash": "sha256-5AIVFLRcx8YAwbFTVPTs7YhwJH7JyHfAQsvXmpovcXI=",
"lastModified": 1791487292,
"narHash": "sha256-tqM7xDEsaeK1p6VvRygeZ9LkQRpUUkbRT02BHv6hnTM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c9fe7d12cd78d1adcd12dd15e24432dde5b155a0",
"rev": "8edc0c72e3a38faf5434e40d1f19431125fe4b30",
"type": "github"
},
"original": {
@@ -784,11 +806,11 @@
},
"nixpkgs_8": {
"locked": {
"lastModified": 1791456040,
"narHash": "sha256-H83yz/do+hjOO0uujHyH8RgtIQP2v7Mw+cLwcZK/xKc=",
"lastModified": 1792502884,
"narHash": "sha256-IoNisKcGbUjdd45qCqywGMCYmZ5EnkxezvLII0FWwNA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e7439b6b14ad3cc35d05608ebca9bce01a25f5f8",
"rev": "02c087f3fe673c9e317f206a627cb6b910336dbe",
"type": "github"
},
"original": {
@@ -849,6 +871,7 @@
"inputs": {
"ags": "ags",
"ayugram-desktop": "ayugram-desktop",
"helix-steel": "helix-steel",
"home-manager": "home-manager",
"hyprland": "hyprland",
"hyprland-plugins": "hyprland-plugins",
@@ -866,11 +889,11 @@
"russia-blocked-geoip": {
"flake": false,
"locked": {
"lastModified": 1791541118,
"narHash": "sha256-nFzXcNfeaujgzCkwTVxk+FJO5fpnGjN0iFjoO+rSHpI=",
"lastModified": 1792592746,
"narHash": "sha256-hDb67lqlxWt3iBsRvxWdE7G4Eg+JEyNRQo7OTqzaDRI=",
"owner": "runetfreedom",
"repo": "russia-blocked-geoip",
"rev": "8a0bb95551940aa0f88e1699687160d19ac1ea51",
"rev": "8898c3b0d779b8aa05b933b97022d7458af2df8d",
"type": "github"
},
"original": {
@@ -881,6 +904,27 @@
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"helix-steel",
"nixpkgs"
]
},
"locked": {
"lastModified": 1775358767,
"narHash": "sha256-f2eC+WIfhjevCPQILuV08i/kmKZzYZpUvkom/33VxCA=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "20fd44bc663daa53a2575e01293e24e681d62244",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"rust-overlay_2": {
"inputs": {
"nixpkgs": [
"yazi",
@@ -906,11 +950,11 @@
"nixpkgs": "nixpkgs_5"
},
"locked": {
"lastModified": 1791103873,
"narHash": "sha256-nFxM+pKoZ8LJAEnUXARyCaOAloWgaW9kZQOSjWzKcTE=",
"lastModified": 1792056300,
"narHash": "sha256-YU1LjDe1o2Ymg5zEiCHbmMXLkDCtxHQRiVqZ3nC5GGk=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "dcd241ba97088c22569d1573286e1b9daad340c0",
"rev": "44931708c62937c2b1bf7953902989fa5bb9ee1b",
"type": "github"
},
"original": {
@@ -937,11 +981,11 @@
"tinted-zed": "tinted-zed"
},
"locked": {
"lastModified": 1790880877,
"narHash": "sha256-j42XqSKOAtuqtfHiElyHre2YS/h3Y3q1DjFnKODN1/k=",
"lastModified": 1791574276,
"narHash": "sha256-UMnLnZ3JlA7UTBNNNx1rNsXYoPP/cirXLCKGiQ/yDx8=",
"owner": "danth",
"repo": "stylix",
"rev": "7c065d1ed05381fceb2403b963c5ad150f32fe39",
"rev": "2856c4c6a845b9bec98828ef197abe176e4806bb",
"type": "github"
},
"original": {
@@ -1004,11 +1048,11 @@
]
},
"locked": {
"lastModified": 1789877109,
"narHash": "sha256-gFYDctvZQdqpKkSYf/QJZwRkQM2YPQ3wHfUxHIP8cDM=",
"lastModified": 1791698670,
"narHash": "sha256-D4GTULEDsGmk4ue01cetWZ6E/emb3FrUys612rg7a5E=",
"owner": "ndfined-crp",
"repo": "tg_owt",
"rev": "25a781fb72647a14adee9f764761c5c8b773c6b0",
"rev": "29330d167ede474885e8aa87b037769cb1cabfc9",
"type": "github"
},
"original": {
@@ -1126,14 +1170,14 @@
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_7",
"rust-overlay": "rust-overlay"
"rust-overlay": "rust-overlay_2"
},
"locked": {
"lastModified": 1791533868,
"narHash": "sha256-KcNMm4C+07B4ukgs23tQYUi8DDGljSdQBZl3+Ibt0Pk=",
"lastModified": 1792507858,
"narHash": "sha256-EEfOrxveuFcXigE5frf6U5rzw1JdzRZ9IcSZzysnHGA=",
"owner": "sxyazi",
"repo": "yazi",
"rev": "1091972581e0d23ed575be967e9a6ef75a17474b",
"rev": "7c45837d553b6cb8032bd7fec2106fdb628e4017",
"type": "github"
},
"original": {
@@ -1145,11 +1189,11 @@
"yazi-plugins": {
"flake": false,
"locked": {
"lastModified": 1790956605,
"narHash": "sha256-/BNGoWziHIZ9i+RoTWGq/q3ZowNCyHGBOWiz8v2/vOE=",
"lastModified": 1792088615,
"narHash": "sha256-3ddGRQSzxyurNcyYJA+9LTLXTCjBFGATTqdkuGKixzA=",
"owner": "yazi-rs",
"repo": "plugins",
"rev": "6229767f7fef39a2a78f5cee9122cc4dfb43f327",
"rev": "9166202cc1694c98bac3a639874d6f4c0eaa2dab",
"type": "github"
},
"original": {
@@ -1164,11 +1208,11 @@
"zapret-flowseal": "zapret-flowseal"
},
"locked": {
"lastModified": 1790422884,
"narHash": "sha256-va22R1zFF7JmibmFwezLYfRup8PfVO3pV7toQH+cbL0=",
"lastModified": 1792489877,
"narHash": "sha256-4/g+qlw4G+2ACSww4vaRsJVLihREjf0eBwofDmeuTns=",
"owner": "kartavkun",
"repo": "zapret-discord-youtube",
"rev": "b82f45c363506588afbb45fefe41fe4670ae1480",
"rev": "69b88b5e51242ddacd87109f31e028e1bef553f6",
"type": "github"
},
"original": {
@@ -1180,11 +1224,11 @@
"zapret-flowseal": {
"flake": false,
"locked": {
"lastModified": 1790845315,
"narHash": "sha256-zhyCW0LFkWkByHu2Vfgita8Bh3lIexUemGyz9elekUQ=",
"lastModified": 1792471264,
"narHash": "sha256-5qKg2BvW4XcjY+Az9ocvwACcfsfBpG4I3Xh6SCPksIM=",
"owner": "Flowseal",
"repo": "zapret-discord-youtube",
"rev": "eb16c1419057e36b8a36fbe7c959483450e6ac8b",
"rev": "176f56dc3f11ce5915c26abcdde903f93d835b64",
"type": "github"
},
"original": {
+5
View File
@@ -52,6 +52,11 @@
inputs.nixpkgs.follows = "nixpkgs";
};
helix-steel = {
url = "github:mattwparas/helix/steel-event-system";
inputs.nixpkgs.follows = "nixpkgs";
};
russia-blocked-geoip = {
url = "github:runetfreedom/russia-blocked-geoip/release";
flake = false;
+2 -1
View File
@@ -11,6 +11,8 @@
./modules/grab.nix
./modules/thinkpad.nix
./modules/fingerprint.nix
./secrets/secrets.nix
./modules/bottles.nix
(import ../modules/common.nix {
inherit lib;
@@ -32,7 +34,6 @@
host.laptop = true;
programs.gamemode.enable = true;
hardware.bluetooth.enable = true;
services.printing.enable = true;
}
+5
View File
@@ -0,0 +1,5 @@
{pkgs-stable, ...}: {
environment.systemPackages = with pkgs-stable; [
bottles
];
}
+311 -16
View File
@@ -1,20 +1,315 @@
{ config, ... }: {
services.fprintd.enable = true;
{ lib, pkgs, ... }: let
pythonValidityPatch = pkgs.writeText "python-validity-python-3.14.patch" ''
diff --git a/validitysensor/flash.py b/validitysensor/flash.py
index d37f15a..be3d4a1 100644
--- a/validitysensor/flash.py
+++ b/validitysensor/flash.py
@@ -34,6 +34,10 @@ def get_flash_info():
rsp = tls.cmd(unhex('3e'))
assert_status(rsp)
rsp = rsp[2:]
+ if len(rsp) < 0xe:
+ # An uninitialised 06cb:009a returns only the success status word.
+ # Let init_flash() format it instead of failing in struct.unpack().
+ return FlashInfo(None, 0, 0, 0, 0, [])
hdr = rsp[:0xe]
rsp = rsp[0xe:]
jid0, jid1, blocks, unknown0, blocksize, unknown1, pcnt = unpack('<HHHHHHH', hdr)
diff --git a/validitysensor/init.py b/validitysensor/init.py
index 18890c5..710668c 100644
--- a/validitysensor/init.py
+++ b/validitysensor/init.py
@@ -25,8 +25,8 @@ def close():
security.pam.services = {
sudo.fprintAuth = true;
login.fprintAuth = false;
hyprlock.fprintAuth = false;
};
security.polkit.extraConfig = ''
polkit.addRule(function(action, subject) {
if (action.id == "net.reactivated.fprint.device.enroll" &&
subject.isInGroup("fprint")) {
return polkit.Result.YES;
}
});
def open_common():
init_data_dir()
- init_flash()
usb.send_init()
+ init_flash()
tls.parse_tls_flash(read_tls_flash())
tls.open()
upload_fwext()
diff --git a/validitysensor/usb.py b/validitysensor/usb.py
index 9db2b07..353b2c7 100644
--- a/validitysensor/usb.py
+++ b/validitysensor/usb.py
@@ -1,6 +1,7 @@
import errno
import logging
+import time
import typing
from binascii import hexlify, unhexlify
from enum import Enum
from struct import unpack
@@ -68,22 +69,32 @@ class Usb:
def close(self):
if self.dev is not None:
try:
self.dev.reset()
self.dev = None
except:
pass
${" "}
def usb_dev(self):
return self.dev
${" "}
+ def cmd_retry_busy(self, command: bytes):
+ # Metallica sensors can report 0x0104 for a few seconds after wake-up.
+ for _ in range(20):
+ rsp = self.cmd(command)
+ status, = unpack('<H', rsp[:2])
+ if status != 0x0104:
+ return rsp
+ time.sleep(0.5)
+ return rsp
+
def send_init(self):
# self.dev.set_configuration()
${" "}
# TODO analyse responses, detect hardware type
- assert_status(self.cmd(unhexlify('01'))) # RomInfo.get()
+ assert_status(self.cmd_retry_busy(unhexlify('01'))) # RomInfo.get()
assert_status(self.cmd(unhexlify('19')))
${" "}
# 43 -- get partition header(?) (02 -- fwext partition)
# c28c745a in response is a FwextBuildtime = 0x5A748CC2
- rsp = self.cmd(unhexlify('4302')) # get_fw_info()
+ rsp = self.cmd_retry_busy(unhexlify('4302')) # get_fw_info()
${" "}
'';
users.users.kirill.extraGroups = [ "fprint" ];
python-validity = pkgs.python3Packages.buildPythonApplication rec {
pname = "python-validity";
version = "0.15-unstable-2020-12-14";
pyproject = true;
src = pkgs.fetchFromGitHub {
owner = "uunicorn";
repo = "python-validity";
rev = "a6bbc21dce7b8b3c3cd92378a0b2579a2fb45920";
hash = "sha256-RflX7e6nd11pSg8mh3mjZiVGNUSdox/SKXHR4W+PhMs=";
};
postPatch = ''
${lib.getExe pkgs.gitMinimal} apply ${pythonValidityPatch}
'';
build-system = [ pkgs.python3Packages.setuptools ];
dependencies = with pkgs.python3Packages; [
cryptography
dbus-python
pygobject3
pyusb
pyyaml
];
nativeBuildInputs = [ pkgs.makeWrapper ];
postInstall = ''
install -Dm755 dbus_service/dbus-service \
"$out/lib/python-validity/dbus-service"
install -Dm644 dbus_service/io.github.uunicorn.Fprint.conf \
"$out/share/dbus-1/system.d/io.github.uunicorn.Fprint.conf"
install -Dm755 dbus_service/dbus-service \
"$out/bin/python-validity-service"
install -Dm755 scripts/factory-reset.py \
"$out/bin/validity-factory-reset"
install -d "$out/share/python-validity/playground"
install -m755 \
scripts/dbus-cmd.py \
scripts/lsdbus.py \
scripts/factory-reset.py \
scripts/prototype.py \
"$out/share/python-validity/playground/"
'';
postFixup = ''
wrapProgram "$out/bin/validity-sensors-firmware" \
--prefix PATH : ${lib.makeBinPath [
pkgs.findutils
pkgs.innoextract
]}
'';
# Upstream does not have an automated test suite.
doCheck = false;
meta = {
description = "Userspace driver for Validity and Synaptics fingerprint sensors";
homepage = "https://github.com/uunicorn/python-validity";
license = lib.licenses.mit;
platforms = lib.platforms.linux;
};
};
fprintd-clients = pkgs.runCommand "fprintd-clients" { } ''
mkdir -p "$out/bin"
for client in fprintd-delete fprintd-enroll fprintd-list fprintd-verify; do
ln -s "${pkgs.fprintd}/bin/$client" "$out/bin/$client"
done
'';
systemctl = lib.getExe' pkgs.systemd "systemctl";
busctl = lib.getExe' pkgs.systemd "busctl";
timeout = lib.getExe' pkgs.coreutils "timeout";
sleep = lib.getExe' pkgs.coreutils "sleep";
fprintdList = lib.getExe' pkgs.fprintd "fprintd-list";
# python-validity initializes the sensor before it registers with
# open-fprintd, so a successful ListEnrolledFingers call means the sensor is
# usable. This is used as ExecStartPost and by greetd ordering.
waitForFprint = pkgs.writeShellScript "wait-for-fprint" ''
for ((i = 0; i < 120; i++)); do
devices="$(${timeout} 1 ${busctl} --system call \
net.reactivated.Fprint \
/net/reactivated/Fprint/Manager \
net.reactivated.Fprint.Manager GetDevices 2>/dev/null || true)"
if [[ "$devices" =~ ^ao\ [1-9] ]]; then
echo "wait-for-fprint: sensor initialized: $devices"
exit 0
fi
${sleep} 0.5
done
echo "wait-for-fprint: sensor initialization timeout" >&2
exit 1
'';
# The xHCI controller is reset during resume, which invalidates
# python-validity's USB handle. Restart it from a systemd-sleep hook: the
# hook runs before logind emits PrepareForSleep(false), so hyprlock's
# built-in fingerprint auth sees a working sensor as soon as it resumes.
fprintResumeHook = pkgs.writeShellScript "fprint-resume" ''
if [ "$1" != "post" ]; then
exit 0
fi
# Stop the old instance first so its stale D-Bus target cannot be selected
# by the readiness checks below. Start without blocking because
# python-validity's ExecStartPost waits for fprintd, but open-fprintd is
# still suspended until Resume is called below.
${timeout} 10 ${systemctl} stop python3-validity.service || true
${systemctl} start --no-block python3-validity.service || true
# Clear open-fprintd's suspended flag. This also unblocks the new
# python-validity instance's ExecStartPost.
${timeout} 10 ${busctl} call net.reactivated.Fprint /net/reactivated/Fprint/Manager \
net.reactivated.Fprint.Manager Resume || true
# Wait for the new backend to register.
for ((i = 0; i < 30; i++)); do
if ${timeout} 0.5 ${fprintdList} root >/dev/null 2>&1; then
break
fi
${sleep} 0.1
done
# The target now exists, so Resume reinitializes the sensor. Wait for it
# to become responsive before logind starts hyprlock's fingerprint auth.
${timeout} 10 ${busctl} call net.reactivated.Fprint /net/reactivated/Fprint/Manager \
net.reactivated.Fprint.Manager Resume || true
for ((i = 0; i < 30; i++)); do
if ${timeout} 0.5 ${fprintdList} root >/dev/null 2>&1; then
exit 0
fi
${sleep} 0.1
done
echo "fprint-resume: fingerprint backend did not become ready" >&2
'';
sleepTargets = [
"suspend.target"
"hibernate.target"
"hybrid-sleep.target"
"suspend-then-hibernate.target"
];
in {
services = {
# The 06cb:009a sensor is driven by python-validity through open-fprintd;
# stock fprintd/libfprint cannot handle it.
fprintd.enable = false;
dbus.packages = [
pkgs.open-fprintd
python-validity
];
};
security.pam.services = {
greetd.fprintAuth = true;
polkit-1.fprintAuth = true;
sudo.fprintAuth = true;
login.fprintAuth = true;
# hyprlock still needs a PAM service for password authentication, but it
# must not run pam_fprintd: its own D-Bus fingerprint implementation is
# used instead. Two implementations would fight over the device, and the
# PAM one breaks when the sensor is reset across suspend.
hyprlock = {};
};
environment.etc."systemd/system-sleep/50-fprint-resume".source = fprintResumeHook;
systemd.services = {
open-fprintd = {
description = "Open FPrint daemon";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "dbus";
BusName = "net.reactivated.Fprint";
ExecStart = "${pkgs.open-fprintd}/lib/open-fprintd/open-fprintd";
Restart = "on-failure";
};
};
python3-validity = {
description = "python-validity fingerprint sensor backend";
wantedBy = [ "multi-user.target" ];
after = [ "open-fprintd.service" ];
wants = [ "open-fprintd.service" ];
serviceConfig = {
Type = "simple";
ExecStart = "${python-validity}/bin/python-validity-service";
ExecStartPost = "-${waitForFprint}";
Restart = "always";
RestartSec = 2;
RuntimeDirectory = "python-validity";
RuntimeDirectoryMode = "0700";
RuntimeDirectoryPreserve = "yes";
};
};
open-fprintd-suspend = {
description = "Suspend open-fprintd backends";
wantedBy = sleepTargets;
before = sleepTargets;
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.open-fprintd}/lib/open-fprintd/suspend.py";
};
};
};
# The sleep hook above resumes open-fprintd before logind wakes the
# graphical session. The stock open-fprintd-resume unit runs later and
# resets the sensor while hyprlock is already verifying, so it must not be
# enabled. Order greetd (and therefore Hyprland/hyprlock) after the
# fingerprint backend so the first lock screen can use the sensor.
systemd.services.greetd = {
after = [ "python3-validity.service" ];
wants = [ "python3-validity.service" ];
};
environment.systemPackages = [
fprintd-clients
python-validity
];
}
+5 -2
View File
@@ -13,7 +13,6 @@
services = {
fwupd.enable = true;
thermald.enable = true;
fprintd.enable = true;
logind.settings.Login = {
HandleLidSwitch = "suspend-then-hibernate";
@@ -21,7 +20,10 @@
HandlePowerKey = "poweroff";
};
tlp.settings = {
tlp = {
enable = true;
settings = {
START_CHARGE_THRESH_BAT0 = 70;
STOP_CHARGE_THRESH_BAT0 = 95;
START_CHARGE_THRESH_BAT1 = 70;
@@ -36,6 +38,7 @@
USB_AUTOSUSPEND = 1;
};
};
};
networking.networkmanager.wifi.powersave = true;
+7
View File
@@ -0,0 +1,7 @@
{
sops = {
age.keyFile = "/root/age.key";
secrets = { };
};
}
+3 -2
View File
@@ -5,14 +5,15 @@
./user/sleepMode.nix
./user/ghostty.nix
./user/qt.nix
./user/zed-idea.nix
./user/helix.nix
#./user/zed-idea.nix
./user/walker.nix
./user/btop.nix
./user/zsh.nix
./user/packages/utils.nix
./user/packages/desktop.nix
./user/packages/kdenlive.nix
./user/quickshell.nix
./user/wob.nix
./user/secrets.nix
];
}
+192
View File
@@ -0,0 +1,192 @@
{ inputs, pkgs, lib, config, ... }: let
# glibc >= 2.44 defines bsearch() as a C23 const-generic macro, which clashes
# with the copy vendored by some tree-sitter grammars (tree-sitter-perl).
# Drop the vendored implementation, same as nixpkgs does for upstream helix.
patchedStdenv = pkgs.stdenv // {
mkDerivation = args: pkgs.stdenv.mkDerivation (args // {
postPatch = (args.postPatch or "") + ''
if [ -e src/bsearch.c ]; then
rm src/bsearch.c
substituteInPlace src/tsp_unicode.h \
--replace-fail '#include "bsearch.c"' ""
fi
'';
});
};
helixGrammars = pkgs.callPackage "${inputs.helix-steel}/grammars.nix" {
stdenv = patchedStdenv;
};
helixSteel =
(
pkgs.callPackage "${inputs.helix-steel}/default.nix" {
gitRev = inputs.helix-steel.rev;
# The upstream flake builds the grammars with its own callPackage,
# swap in the patched ones.
callPackage = path: args:
if toString path == toString "${inputs.helix-steel}/grammars.nix" then helixGrammars
else pkgs.callPackage path args;
}
)
.overrideAttrs (old: {
cargoBuildFlags = (old.cargoBuildFlags or []) ++ [
"--features"
"steel,git"
];
});
comment-headers = pkgs.fetchurl {
url = "https://github.com/codingvivi/comment-dividers.hx/raw/refs/heads/main/comment_headers.scm";
name = "comment_headers.scm";
hash = "sha256-ohgn6DFJl0rdRJuri6Frs/uUlUsPxMvPHDuw4FS3qEE=";
};
patched-comment-headers = pkgs.runCommand "comment_headers.scm" { } ''
install -m 0644 ${comment-headers} "$out"
${pkgs.python3}/bin/python3 - <<'PY'
from pathlib import Path
import os
import sys
p = Path(os.environ["out"])
s = p.read_text()
start_marker = "(define (get-block-delims)"
end_marker = "(define (delete_till_first_nonwhitespace!)"
try:
start = s.index(start_marker)
end = s.index(end_marker, start)
except ValueError as e:
sys.exit(f"failed to find function markers: {e}")
new_get_block_delims = """(define (get-block-delims)
(helix.static.open_above)
(helix.static.insert_string "test_string")
(helix.static.normal_mode)
(helix.static.extend_to_line_bounds)
(helix.static.trim_selections)
(helix.static.toggle_comments)
(let* ([commented-test-string (helix.static.current-highlighted-text!)]
[strings-without-whitespace (split-whitespace commented-test-string)])
(delete-line-noyank!)
(cond
[(and (> (length strings-without-whitespace) 1)
(equal? (last strings-without-whitespace) "test_string"))
(list (first strings-without-whitespace) "")]
[(> (length strings-without-whitespace) 2)
(list (first strings-without-whitespace) (last strings-without-whitespace))]
[else
(list "" "")])))
"""
s = s[:start] + new_get_block_delims + s[end:]
old = "(make-line line-length fill-pattern #f padding-pattern)"
new = "(make-line line-length \"\" fill-pattern padding-pattern)"
if old not in s:
sys.exit("failed to find insert-divider make-line call")
s = s.replace(old, new)
p.write_text(s)
PY
'';
in {
home.packages = with pkgs; [
steel
wakatime-cli
];
# Wakapi is WakaTime-compatible, so the same plugin/client works.
sops.templates.".wakatime.cfg" = {
path = "${config.home.homeDirectory}/.wakatime.cfg";
mode = "0600";
content = ''
[settings]
api_url = https://coding.srcoder.moe/api
api_key = ${config.sops.placeholder."tokens/apis/wakapi"}
'';
};
home.activation.installWakatimeHx = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
export PATH="${pkgs.steel}/bin:$PATH"
export STEEL_HOME="${config.xdg.dataHome}/steel"
if [ ! -f "$STEEL_HOME/cogs/wakatime/wakatime.scm" ]; then
forge pkg install --git https://github.com/Xerxes-2/wakatime.hx.git
fi
'';
programs.helix = {
enable = true;
package = helixSteel;
defaultEditor = true;
settings = {
# theme = "catppuccin-mocha";
editor = {
insert-final-newline = false;
indent-guides.render = true;
whitespace.render = {
space = "all";
tab = "all";
nbsp = "none";
nnbsp = "none";
newline = "none";
};
cursor-shape = {
normal = "block";
insert = "bar";
select = "underline";
};
};
keys.normal = {
space = {
C-c = {
h = ":comment-h1";
j = ":comment-h2";
k = ":comment-h3";
d = ":comment-divider";
};
};
S-down = ["extend_to_line_bounds" "delete_selection" "paste_after"];
S-up = ["extend_to_line_bounds" "delete_selection" "move_line_up" "paste_before"];
};
};
};
xdg.configFile = {
"helix/cogs/comment_headers.scm".source = patched-comment-headers;
"helix/helix.scm".text = ''
(require (prefix-in div. "cogs/comment_headers.scm"))
(provide comment-h1 comment-h2 comment-h3 comment-divider)
(define (comment-h1)
(div.insert-h1!))
(define (comment-h2)
(div.insert-h2!))
(define (comment-h3)
(div.insert-h3!))
(define (comment-divider)
(div.insert-divider! 80 "~"))
'';
"helix/init.scm".text = ''
;; Steel init file
(require "wakatime/wakatime.scm")
'';
};
}
-5
View File
@@ -1,5 +0,0 @@
{ pkgs, ... }: {
home.packages = with pkgs; [
heroic
];
}
-5
View File
@@ -1,5 +0,0 @@
{ pkgs, ... }: {
home.packages = with pkgs; [
kdePackages.kdenlive
];
}
+9
View File
@@ -0,0 +1,9 @@
{ config, ... }: {
sops = {
defaultSopsFile = ../../user/${config.home.username}/secrets.yaml;
age.keyFile = "${config.home.homeDirectory}/.config/sops/age/keys.txt";
secrets."tokens/apis/wallhaven" = {};
secrets."tokens/apis/wakapi" = {};
};
}
+55 -2
View File
@@ -1,8 +1,50 @@
{ pkgs, lib, ... }:
{ config, pkgs, lib, ... }:
let
hyprLua = import ./hyprland/lua.nix { inherit lib; };
hyprlock = lib.getExe pkgs.hyprlock;
hyprctl = lib.getExe' config.wayland.windowManager.hyprland.package "hyprctl";
loginctl = lib.getExe' pkgs.systemd "loginctl";
pidof = lib.getExe' pkgs.procps "pidof";
fprintdList = lib.getExe' pkgs.fprintd "fprintd-list";
timeout = lib.getExe' pkgs.coreutils "timeout";
sleepCmd = lib.getExe' pkgs.coreutils "sleep";
sh = lib.getExe pkgs.bash;
hyprlockWait = pkgs.writeShellScript "hyprlock-wait-fprint" ''
for i in $(seq 1 60); do
if ${timeout} 1 ${fprintdList} kirill >/dev/null 2>&1; then
break
fi
${sleepCmd} 1
done
exec ${hyprlock}
'';
in {
wayland.windowManager.hyprland.settings.on = [ (hyprLua.onStart [ "hyprlock" ]) ];
wayland.windowManager.hyprland.settings.on = [ (hyprLua.onStart [ "${hyprlockWait}" ]) ];
# Hypridle listens to logind's PrepareForSleep signal: it locks the session
# before suspend and makes sure hyprlock is running (and the display is on)
# after resume.
services.hypridle = {
enable = true;
settings = {
general = {
lock_cmd = "${pidof} hyprlock >/dev/null || ${hyprlockWait}";
before_sleep_cmd = "${loginctl} lock-session";
after_sleep_cmd = "${pidof} hyprlock >/dev/null || ${hyprlockWait} & ${hyprctl} dispatch 'hl.dsp.dpms({action = \"on\"})'";
};
# Hypridle requires at least one listener, so keep a no-op timer that
# practically never fires. Screen locking is driven by sleep/resume only.
listener = [
{
timeout = 604800; # 7 days
on-timeout = "";
}
];
};
};
programs.hyprlock = let
image = pkgs.fetchurl {
@@ -14,6 +56,17 @@ in {
enable = true;
settings = {
# Hyprlock's built-in fingerprint support watches logind's
# PrepareForSleep signal and re-arms itself after resume. PAM-based
# fingerprint auth (security.pam.services.hyprlock.fprintAuth) does not
# survive the fingerprint device being reset during suspend.
auth.fingerprint = {
enabled = true;
ready_message = "Scan fingerprint to unlock";
present_message = "Scanning...";
retry_delay = 250;
};
background = {
path = lib.mkForce "${image}";
blur_passes = 0;
+2
View File
@@ -91,6 +91,8 @@
swichWalls = "wallpaper_changer";
shell = "nix-shell . --run zsh";
testZapret = "sudo zapret-test-strategies";
charge = "sudo tlp setcharge 90 95 BAT0 && sudo tlp setcharge 90 95 BAT1 && echo 'Зарядка обеих батарей до 95% запущена (только на текущую сессию)'";
};
initContent = ''
+279
View File
@@ -0,0 +1,279 @@
# Секреты (sops-nix)
Кратко: как всё устроено, как добавить новый секрет и как поднять рабочий sops на новом устройстве.
## Как устроено
- `.sops.yaml` в корне репозитория — список age-реципиентов и правила выбора ключа по пути файла.
- Секреты делятся на два уровня:
- **host** (системные, читает root): зашифрованные файлы в `host/<Host>/secrets/<файл>`, метаданные в `host/<Host>/secrets/secrets.nix`, подключается из `host/<Host>/configuration.nix` через `imports`. При активации расшифровываются в `/run/secrets/<имя>`.
- **user** (пользовательские, читает home-manager): один файл `user/<username>/secrets.yaml`, метаданные в `modules/user/secrets.nix`, подключается из `modules/user.nix` через `imports`. При активации расшифровываются в `~/.config/sops-nix/secrets/<имя>`.
- Расшифровка идёт в runtime, в `/nix/store` секреты не попадают.
- age-ключи выводятся из существующих SSH-ключей:
- host — `/root/age.key` (читает root при активации),
- user — `~/.config/sops/age/keys.txt` (читает пользователь при активации home-manager).
Текущие правила в `.sops.yaml`:
```yaml
keys:
- &key age1tx6jdr6535xxeygpwkkgvdwc24tulr420cm7gk56xzfp0slzeqks2nwpyg
creation_rules:
- path_regex: host/[^/]+/secrets/.+$
key_groups:
- age:
- *key
- path_regex: user/[^/]+/secrets\.yaml$
key_groups:
- age:
- *key
```
Правило выбирается по пути файла. Важно: `path_regex` проверяется против пути **входного** файла относительно корня репозитория, поэтому открытое значение нужно создавать по целевому пути (см. примеры ниже).
## Host-секрет (системный)
### Добавить новый
1. Придумай имя, дальше для примера — `mysecret`.
2. Положи открытое значение во временный файл **внутри** `host/<Host>/secrets/` (путь важен — см. `path_regex`):
```bash
printf '%s' 'СЕКРЕТНОЕ_ЗНАЧЕНИЕ' > host/ThinkPad/secrets/mysecret.plain
```
3. Зашифруй (приватный ключ не нужен — sops берёт публичного реципиента из `.sops.yaml`). Флаги указываются **до** позиционных аргументов:
```bash
nix-shell -p sops --run \
"sops --input-type binary --output-type binary \
--encrypt --output host/ThinkPad/secrets/mysecret.enc \
host/ThinkPad/secrets/mysecret.plain"
rm host/ThinkPad/secrets/mysecret.plain
```
`--input-type`/`--output-type` обязательны для бинарных данных, иначе sops пытается определить тип по расширению.
4. Опиши секрет в `host/<Host>/secrets/secrets.nix`:
```nix
{
sops = {
age.keyFile = "/root/age.key";
secrets = {
mysecret = {
format = "binary"; # yaml | json | binary | dotenv | ini
sopsFile = ./mysecret.enc;
owner = "kirill"; # владелец расшифрованного файла
mode = "0400";
};
};
};
}
```
`format = "binary"` — для произвольных байтов (`.pem`, токены). Для структурированных данных используй `yaml`/`json`.
`owner`/`mode` нужны, если секрет читает пользовательский процесс (а не root-сервис).
5. `git add` — **Nix видит только файлы, добавленные в git** — и ребилд:
```bash
git add host/ThinkPad/secrets/
sudo nixos-rebuild switch --flake .#ThinkPad
```
6. Проверка:
```bash
sudo ls -l /run/secrets/mysecret
```
### Изменить или посмотреть существующий секрет
Используется приватный ключ (`/root/age.key`), поэтому нужен root:
```bash
sudo SOPS_AGE_KEY_FILE=/root/age.key nix-shell -p sops --run \
"sops --input-type binary --output-type binary host/ThinkPad/secrets/mysecret.enc"
```
`sops` сам не ищет `/root/age.key`, путь передаётся через `SOPS_AGE_KEY_FILE`.
Для бинарных файлов нужны `--input-type`/`--output-type`, иначе sops не сможет определить формат.
## User-секрет (home-manager)
Все пользовательские секреты лежат в одном файле `user/<username>/secrets.yaml`. Значения — вложенные ключи YAML, а имя секрета в nix-коде совпадает с путём этого ключа (`/` = вложенность). Например, секрет `tokens/apis/wallhaven` читается из:
```yaml
tokens:
apis:
wallhaven: ЗНАЧЕНИЕ
```
Путь расшифрованного файла: `~/.config/sops-nix/secrets/tokens/apis/wallhaven`.
### Добавить новый
1. Придумай имя, дальше для примера — `tokens/apis/wallhaven`.
2. Добавь значение в общий файл секретов (sops откроет расшифрованное содержимое в `$EDITOR` и зашифрует при сохранении; путь совпадает с `path_regex`, поэтому правило применится автоматически):
```bash
nix-shell -p sops --run "sops user/kirill/secrets.yaml"
```
Если файла ещё нет, создай его сразу по целевому пути и зашифруй на месте:
```bash
printf 'tokens:\n apis:\n wallhaven: ЗНАЧЕНИЕ\n' > user/kirill/secrets.yaml
nix-shell -p sops --run "sops --encrypt --in-place user/kirill/secrets.yaml"
```
3. Опиши секрет в `modules/user/secrets.nix`:
```nix
{ config, ... }: {
sops = {
defaultSopsFile = ../../user/${config.home.username}/secrets.yaml;
age.keyFile = "${config.home.homeDirectory}/.config/sops/age/keys.txt";
secrets."tokens/apis/wallhaven" = {};
};
}
```
4. Используй в home-manager конфиге через путь:
```nix
config.sops.secrets."tokens/apis/wallhaven".path
```
5. `git add` — **Nix видит только файлы, добавленные в git** — и ребилд:
```bash
git add user/kirill/secrets.yaml modules/user/secrets.nix modules/user.nix
sudo nixos-rebuild switch --flake .#ThinkPad
```
6. Проверка:
```bash
ls -l ~/.config/sops-nix/secrets/tokens/apis/wallhaven
```
### Изменить или посмотреть существующий секрет
```bash
nix-shell -p sops --run "sops user/kirill/secrets.yaml"
```
sops подхватывает `~/.config/sops/age/keys.txt` автоматически. Если путь другой — задай `SOPS_AGE_KEY_FILE`.
## Новое устройство
1. Убедись, что на устройстве есть приватный SSH-ключ, из которого выводится age-ключ. Ключ должен быть **без пароля** — иначе расшифровка при активации будет требовать ввода. На текущей машине это `~/.ssh/id_ed25519_sops`.
2. Создай age-ключ хоста (root читает его при активации):
```bash
sudo nix-shell -p ssh-to-age --run \
"ssh-to-age -private-key -i /home/kirill/.ssh/id_ed25519_sops -o /root/age.key"
sudo chmod 600 /root/age.key
```
3. Создай age-ключ пользователя (home-manager читает его при активации):
```bash
mkdir -p ~/.config/sops/age
nix-shell -p ssh-to-age --run \
"ssh-to-age -private-key -i ~/.ssh/id_ed25519_sops -o ~/.config/sops/age/keys.txt"
chmod 700 ~/.config/sops/age
chmod 600 ~/.config/sops/age/keys.txt
```
4. Проверь, что публичный age-реципиент устройства совпадает с тем, что в `.sops.yaml`:
```bash
nix-shell -p ssh-to-age --run \
"ssh-to-age -i ~/.ssh/id_ed25519_sops.pub"
```
Должно вывести:
```
age1tx6jdr6535xxeygpwkkgvdwc24tulr420cm7gk56xzfp0slzeqks2nwpyg
```
Если вывод другой — добавь новый реципиент (см. следующий раздел).
5. Создай структуру секретов хоста:
```
host/<Host>/secrets/secrets.nix
host/<Host>/secrets/<файлы секретов>
```
Минимальный `secrets.nix`:
```nix
{
sops = {
age.keyFile = "/root/age.key";
secrets = { };
};
}
```
И добавь `./secrets/secrets.nix` в `imports` в `host/<Host>/configuration.nix`.
6. Для пользовательских секретов создай `user/<username>/secrets.yaml` (см. раздел «User-секрет») и убедись, что `modules/user.nix` подключает `./user/secrets.nix`.
7. `git add` + ребилд:
```bash
git add host/<Host>/secrets/ user/<username>/secrets.yaml modules/user/secrets.nix
sudo nixos-rebuild switch --flake .#<Host>
```
## Добавить устройство к существующим секретам
1. Получи age-реципиент устройства (шаг 4 выше).
2. Добавь его в `.sops.yaml` в оба правила:
```yaml
keys:
- &key age1tx6jdr6535xxeygpwkkgvdwc24tulr420cm7gk56xzfp0slzeqks2nwpyg
- &newdevice age1...новый_реципиент... # замени на реальный
creation_rules:
- path_regex: host/[^/]+/secrets/.+$
key_groups:
- age:
- *key
- *newdevice
- path_regex: user/[^/]+/secrets\.yaml$
key_groups:
- age:
- *key
- *newdevice
```
3. Перешифруй существующие секреты под новый список реципиентов (нужен приватный ключ). Для host-секретов — root и `/root/age.key`:
```bash
sudo SOPS_AGE_KEY_FILE=/root/age.key nix-shell -p sops --run \
"sops updatekeys host/ThinkPad/secrets/mysecret.enc"
```
Для user-секретов — пользовательский ключ:
```bash
SOPS_AGE_KEY_FILE=~/.config/sops/age/keys.txt nix-shell -p sops --run \
"sops updatekeys user/kirill/secrets.yaml"
```
4. `git add` + ребилд на этом устройстве.
+170 -2
View File
@@ -1,13 +1,181 @@
{ pkgs, lib, config, inputs, host, ... }:
let
secretPath = config.sops.secrets."tokens/apis/wallhaven".path;
wallpaper_changer = pkgs.writers.writePython3Bin "wallpaper_changer" {
libraries = [ pkgs.python3Packages.requests ];
flakeIgnore = [ "E501" "E111" "E701" "E241" "E731" ];
flakeIgnore = [ "E111" "E121" "E241" "E501" "E701" "E731" ];
} /*py*/ ''
import requests as requests
from random import choice
from os import system, listdir
from os import environ, getuid, listdir, makedirs, remove, replace, system
from os.path import exists, getmtime, isfile
from subprocess import run
notify_id = None
def notify(text, progress=None, expire_time=5000):
global notify_id
command = [
"${pkgs.libnotify}/bin/notify-send",
"--app-name", "wallpaper_changer",
"--print-id",
"--expire-time", str(expire_time),
]
if notify_id:
command.extend(["--replace-id", str(notify_id)])
if progress is not None:
command.extend(["--hint", f"int:value:{progress}"])
command.extend(["Wallpaper", text])
result = run(command, capture_output=True, text=True, check=False)
output = result.stdout.strip()
if output.isdigit():
notify_id = output
def download_with_progress(link, destination):
temporary = f"{destination}.part"
downloaded = 0
last_progress = -1
response = None
try:
response = requests.get(link, stream=True, timeout=30)
response.raise_for_status()
total = int(response.headers.get("content-length", 0))
with open(temporary, "wb") as file:
for chunk in response.iter_content(chunk_size=256 * 1024):
if not chunk:
continue
file.write(chunk)
downloaded += len(chunk)
if total <= 0:
continue
progress = min(100, downloaded * 100 // total)
if progress == 100 or progress >= last_progress + 2:
notify(f"Downloading... {progress}%", progress, 0)
last_progress = progress
replace(temporary, destination)
except Exception:
if exists(temporary):
remove(temporary)
raise
finally:
if response is not None:
response.close()
def prune(folder, keep=20):
files = [
(name, getmtime(f"{folder}/{name}"))
for name in listdir(folder)
if isfile(f"{folder}/{name}") and not name.endswith(".part")
]
if len(files) <= keep:
return
for name, _ in sorted(files, key=lambda item: item[1])[:len(files) - keep]:
remove(f"{folder}/{name}")
def read_history(path):
try:
with open(path) as file:
return [line.strip() for line in file if line.strip()]
except OSError:
return []
def write_history(path, ids):
try:
with open(path, "w") as file:
file.write("\n".join(dict.fromkeys(ids)) + "\n")
except OSError:
pass
folder = "${config.home.homeDirectory}/wallpapers"
url = "https://wallhaven.cc/api/v1/collections/BlackRaven21/2284568"
runtime = environ.get("XDG_RUNTIME_DIR") or f"/run/user/{getuid()}"
history = f"{runtime}/wallpaper_changer_history"
token = None
try:
with open("${secretPath}") as file:
token = file.read().strip() or None
except OSError:
pass
filename = None
notify("Updating wallpaper...", 0)
try:
params = {}
if token:
params["apikey"] = token
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
page_json = response.json()
walls = list(page_json['data'])
for page in range(2, page_json['meta']['last_page'] + 1):
params['page'] = page
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
walls.extend(response.json()['data'])
seen = read_history(history)
candidates = [wall for wall in walls if wall['id'] not in seen]
if not candidates:
seen = []
candidates = walls
wallpaper = choice(candidates)
link = wallpaper['path']
format = wallpaper['file_type']
id = wallpaper['id']
if format == "image/jpeg": ext = "jpg"
else: ext = "png"
filename = f"{id}.{ext}"
destination = f"{folder}/{filename}"
if not exists(destination):
makedirs(folder, exist_ok=True)
notify("Downloading... 0%", 0, 0)
download_with_progress(link, destination)
notify("Downloaded", 100)
prune(folder)
else:
notify("Using cached wallpaper", 100)
write_history(history, seen + [id])
except requests.exceptions.RequestException:
notify("Offline mode")
try:
filename = choice(listdir(folder))
except (FileNotFoundError, IndexError):
notify("Offline mode and wallpaper cache is empty")
finally:
if filename is not None:
notify("Applying wallpaper", 100)
system(f"${lib.getExe pkgs.awww} img {folder}/{filename} --transition-type center")
'';
in {
+19
View File
@@ -0,0 +1,19 @@
tokens:
apis:
wallhaven: ENC[AES256_GCM,data:yO+thw4xgpMBuNx2LyxguDxAzoKSeDCyAAOcgtTxx74=,iv:j3LGKXnmykTamSuuIFbACT0kRDFazOhNEgWtVoxiWK4=,tag:jLaJrcUmDuxVj1D65NZhyg==,type:str]
wakapi: ENC[AES256_GCM,data:B4BOqy6TyZj4qPB9H7Ne+XaW701tX+ho8dMn0EGvqzdVOKq+,iv:/P6KZV9KgIBXslB3yZD5G3XER279iJ6/R2n+ZdTaBLs=,tag:r/SDXjofl8DdDlzP9Xo5mQ==,type:str]
sops:
age:
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1MXcwbkFxZ2R4V0ppOGo3
UmFsTTJGekQ0NGVtTFpCRm50ekxJcEZUUW1FCkF5aStVckVNaVJndmptdnhtNjgx
Z056TVF5ZVRuMWJZKzk1WlRZTXN3bjAKLS0tIEhHd1JyTkhIdjJuZVZtSFlTRXIy
cTRWRkZXUmcrcHRXU2pyTzh1OGhaSkkKiqYLrPm1zS0PfrIeUnZHXvvHcY15PxDS
01jUzubGavkuOXb0hJmr6oBkEkSMBW+GLewduqj0SxGF2yxTSkM0kQ==
-----END AGE ENCRYPTED FILE-----
recipient: age1tx6jdr6535xxeygpwkkgvdwc24tulr420cm7gk56xzfp0slzeqks2nwpyg
lastmodified: "2026-10-21T17:46:52Z"
mac: ENC[AES256_GCM,data:mLdD78pb0Qf30+l/igb+jXMHxNXTXJPNtsi3cGeEgYLKxnx02OVBuOnncMZvN351yh7h5if9pCqwCJeOISWU8uFZrQdJeDjdmIPlP1eqzBQ7fK4fhEeRzAcn4dTQ5tcmu3uXmWElKcBYhMFEPr3/etyfDcX5ZFwOra8wUqU8nq4=,iv:TydKNjO5stupovtCF/rHMtr1Da2IFafMZiLQxsln1uA=,tag:kXl5eELy9I5APJkAQ8tk3w==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.3