Compare commits

..
1 Commits
Author SHA1 Message Date
Alexander Heldt a6fac14f57 manatee/backwards: Move calibre-web module to manatee 2025-05-18 17:39:32 +02:00
128 changed files with 948 additions and 5704 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ let
flakePath = config.config-manager.flakePath;
nixosConfiguration = config.config-manager.nixosConfiguration;
nh = inputs.nh.packages."${pkgs.stdenv.hostPlatform.system}".default;
nh = inputs.nh.packages."${pkgs.system}".default;
config-manager =
if flakePath == "" then
Generated
+40 -885
View File
File diff suppressed because it is too large Load Diff
+3 -62
View File
@@ -12,7 +12,8 @@
};
nh = {
url = "github:viperML/nh/b00a24b39944efd4ec7944f02e0bd9113d991767";
url = "github:viperML/nh";
inputs.nixpkgs.follows = "nixpkgs";
};
nix-gc-env.url = "github:Julow/nix-gc-env";
@@ -32,26 +33,11 @@
inputs.nixpkgs.follows = "nixpkgs";
};
hyprland = {
url = "github:hyprwm/Hyprland";
inputs.nixpkgs.follows = "nixpkgs";
};
hyprland-contrib = {
url = "github:hyprwm/contrib";
inputs.nixpkgs.follows = "nixpkgs";
};
nix-jetbrains-plugins = {
url = "github:nix-community/nix-jetbrains-plugins";
inputs.nixpkgs.follows = "nixpkgs";
};
naviterm = {
url = "gitlab:detoxify92/naviterm";
inputs.nixpkgs.follows = "nixpkgs";
};
pppdotpm-site = {
url = "git+ssh://gitea@git.ppp.pm:1122/alex/ppp.pm-site.git?ref=main";
inputs.nixpkgs.follows = "nixpkgs";
@@ -59,49 +45,6 @@
whib-backend = {
url = "git+ssh://gitea@git.ppp.pm:1122/alex/whib.git?ref=master";
# url = "path:/home/alex/code/own/whib";
inputs.nixpkgs.follows = "nixpkgs";
};
whib-frontend = {
url = "git+ssh://gitea@git.ppp.pm:1122/alex/whib-react.git?ref=master";
# url = "path:/home/alex/code/own/whib-react";
inputs.nixpkgs.follows = "nixpkgs";
};
komga-comictracker = {
url = "git+ssh://gitea@git.ppp.pm:1122/alex/komga-comictracker.git?ref=main";
# url = "path:/home/alex/code/own/komga-comictracker";
inputs.nixpkgs.follows = "nixpkgs";
};
komga-bookmanager = {
url = "git+ssh://gitea@git.ppp.pm:1122/alex/komga-bookmanager.git?ref=main";
# url = "path:/home/alex/code/own/komga-bookmanager";
inputs.nixpkgs.follows = "nixpkgs";
};
komga-reading-stats = {
url = "git+ssh://gitea@git.ppp.pm:1122/alex/komga-reading-stats.git?ref=main";
# url = "path:/home/alex/code/own/komga-reading-stats";
inputs.nixpkgs.follows = "nixpkgs";
};
puppy-tracker = {
url = "git+ssh://gitea@git.ppp.pm:1122/alex/puppy-tracker.git?ref=main";
# url = "path:/home/alex/code/puppy-tracker";
inputs.nixpkgs.follows = "nixpkgs";
};
solo-referee = {
url = "git+ssh://gitea@git.ppp.pm:1122/alex/solo-referee.git?ref=main";
# url = "path:/home/alex/code/solo-referee";
inputs.nixpkgs.follows = "nixpkgs";
};
todo = {
url = "git+ssh://gitea@git.ppp.pm:1122/alex/todo.git?ref=main";
# url = "git+file:///home/alex/code/todo?ref=main";
inputs.nixpkgs.follows = "nixpkgs";
};
};
@@ -157,7 +100,6 @@
./hosts/tadpole/configuration.nix
./hosts/tadpole/home.nix
inputs.whib-backend.nixosModules.${system}.default
inputs.whib-frontend.nixosModules.${system}.default
];
};
@@ -173,7 +115,6 @@
modules = [
./hosts/test-vm/configuration.nix
inputs.whib-backend.nixosModules.${system}.default
inputs.whib-frontend.nixosModules.${system}.default
];
};
};
@@ -185,7 +126,7 @@
in
{
${system}.default = pkgs.mkShell {
packages = [ pkgs.nixfmt ];
packages = [ pkgs.nixfmt-rfc-style ];
};
};
};
+1 -7
View File
@@ -2,6 +2,7 @@
{
imports = [
../../config-manager/default.nix
../../shared-modules/syncthing.nix
./hardware-configuration.nix
./modules
];
@@ -21,13 +22,6 @@
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
extraConfig.pipewire."90-hdmi-fix" = {
"context.properties" = {
"default.clock.rate" = 48000;
"default.clock.allowed-rates" = [ 48000 ];
};
};
};
hardware = {
+1 -1
View File
@@ -8,7 +8,7 @@
};
environment.systemPackages = [
inputs.agenix.packages."${pkgs.stdenv.hostPlatform.system}".default
inputs.agenix.packages."${pkgs.system}".default
];
};
}
-5
View File
@@ -38,11 +38,6 @@ in
efi.canTouchEfiVariables = true;
};
extraModprobeConfig = ''
options snd-intel-dspcfg dsp_driver=1
options snd_hda_intel power_save=0 power_save_controller=N
'';
};
};
}
+2
View File
@@ -13,6 +13,8 @@ in
ssh.enable = true;
git.enable = true;
nginx.enable = true;
syncthing.enable = true;
restic.enable = true;
};
};
}
@@ -46,12 +46,6 @@ in
package = wrapped;
# Pin the legacy profile location. The default changed to the XDG path
# (`$XDG_CONFIG_HOME/mozilla/firefox`) for stateVersion >= "26.05";
# keep the old `~/.mozilla/firefox` so the existing profile isn't
# stranded (migrating would require moving the profile dir by hand).
configPath = ".mozilla/firefox";
profiles = {
alex = {
id = 0;
@@ -2,8 +2,6 @@
{
home-manager.users.alex = {
home.packages = [
pkgs.nethack
pkgs.moonlight-qt
pkgs.pcsx2
+3
View File
@@ -4,3 +4,6 @@
[url "git@github.com:"]
insteadOf = https://github.com/
[url "git@codeberg.org:"]
insteadOf = https://codeberg.org/
+8 -8
View File
@@ -8,14 +8,6 @@
};
};
desktopManager = {
gnome.enable = true;
};
displayManager = {
gdm.enable = true;
};
xserver = {
enable = true;
@@ -23,6 +15,14 @@
layout = "se";
variant = "";
};
desktopManager = {
gnome.enable = true;
};
displayManager = {
gdm.enable = true;
};
};
};
+22 -29
View File
@@ -3,40 +3,33 @@
networking = {
hostName = "backwards";
wireless.enable = false;
networkmanager.enable = false;
networkmanager = {
#wireless.networks are defined in the secret `wpa_supplicant.conf`
wireless = {
enable = true;
wifi.backend = "iwd";
secretsFile = config.age.secrets.wireless-network-secrets.path;
ensureProfiles = {
environmentFiles = [
config.age.secrets.wireless-network-secrets.path
];
networks = {
"w1-f1_5G" = {
pskRaw = "ext:w1-f1_psk";
};
};
};
profiles = {
w1-f1_5G = {
connection = {
id = "w1-f1_5G";
type = "wifi";
interface-name = "wlp1s0";
};
wifi = {
ssid = "w1-f1_5G";
mode = "infrastructure";
};
wifi-security = {
key-mgmt = "wpa-psk";
psk = "$w1_f1_psk";
};
ipv4 = {
method = "manual";
addresses = "192.168.50.202/24";
gateway = "192.168.50.1";
dns = "1.1.1.1";
};
};
defaultGateway = "192.168.50.1";
nameservers = [ "1.1.1.1" ];
interfaces = {
wlp1s0 = {
useDHCP = false;
ipv4 = {
addresses = [
{
address = "192.168.50.202";
prefixLength = 24;
}
];
};
};
};
@@ -0,0 +1,74 @@
{ lib, config, ... }:
let
enabled = config.mod.restic.enable;
in
{
options = {
mod.restic = {
enable = lib.mkEnableOption "Enable restic";
};
};
config = lib.mkIf enabled {
fileSystems."/home/alex/backup" = {
device = "/dev/disk/by-uuid/34601701-65e6-4b2c-ac4d-8bef3dfd743f";
fsType = "ext4";
options = [ "nofail" ];
};
services = {
restic.backups = {
"sync-to-external" = {
initialize = true;
passwordFile = config.age.secrets.restic-password.path;
paths = [ "/home/alex/sync" ];
repository = "/home/alex/backup/restic";
timerConfig = {
OnCalendar = "*-*-* 0/12:00:00"; # Every 12th hour, i.e. twice a day
Persistent = true;
};
pruneOpts = [
"--keep-daily 1"
"--keep-weekly 7"
"--keep-yearly 12"
];
};
"sync-to-cloud" = {
initialize = true;
passwordFile = config.age.secrets.restic-password.path;
environmentFile = config.age.secrets.restic-cloud-sync-key.path;
repositoryFile = config.age.secrets.restic-cloud-sync-repository.path;
paths = [ "/home/alex/sync" ];
exclude = [ "/home/alex/sync/reading-material" ];
timerConfig = {
OnCalendar = "*-*-* 0/12:00:00"; # Every 12th hour, i.e. twice a day
Persistent = true;
};
pruneOpts = [
"--keep-daily 1"
"--keep-weekly 7"
"--keep-yearly 12"
];
};
};
};
age = {
secrets = {
"restic-password".file = ../../../../secrets/backwards/restic-password.age;
"restic-cloud-sync-key".file = ../../../../secrets/backwards/restic-cloud-sync-key.age;
"restic-cloud-sync-repository".file =
../../../../secrets/backwards/restic-cloud-sync-repository.age;
};
};
};
}
+23 -21
View File
@@ -21,37 +21,26 @@ in
home-manager.users.alex = {
programs.ssh = {
enable = true;
enableDefaultConfig = false;
settings = {
matchBlocks = {
"manatee" = {
HostName = "manatee";
User = "alex";
IdentityFile = "/home/alex/.ssh/alex.backwards-manatee";
Port = 1122;
hostname = "manatee";
user = "alex";
identityFile = "/home/alex/.ssh/alex.backwards-manatee";
port = 1122;
};
"git.ppp.pm" = {
HostName = "git.ppp.pm";
IdentityFile = "/home/alex/.ssh/alex.backwards-git.ppp.pm";
hostname = "git.ppp.pm";
identityFile = "/home/alex/.ssh/alex.backwards-git.ppp.pm";
};
"*" = {
ForwardAgent = false;
AddKeysToAgent = "no";
Compression = false;
ServerAliveInterval = 0;
ServerAliveCountMax = 3;
HashKnownHosts = false;
UserKnownHostsFile = "~/.ssh/known_hosts";
ControlMaster = "no";
ControlPath = "~/.ssh/master-%r@%n:%p";
ControlPersist = "no";
"codeberg.org" = {
hostname = "codeberg.org";
identityFile = "/home/alex/.ssh/alex.backwards-codeberg.org";
};
};
};
home.packages = [ pkgs.sshfs ];
};
environment.etc."ssh/authorized_keys_command" = {
@@ -132,6 +121,19 @@ in
owner = "alex";
group = "users";
};
"alex.backwards-codeberg.org" = {
file = ../../../../secrets/backwards/alex.backwards-codeberg.org.age;
path = "/home/alex/.ssh/alex.backwards-codeberg.org";
owner = "alex";
group = "users";
};
"alex.backwards-codeberg.org.pub" = {
file = ../../../../secrets/backwards/alex.backwards-codeberg.org.pub.age;
path = "/home/alex/.ssh/alex.backwards-codeberg.org.pub";
owner = "alex";
group = "users";
};
};
};
}
@@ -0,0 +1,124 @@
{ lib, config, ... }:
let
enabled = config.mod.syncthing.enable;
in
{
options = {
mod.syncthing = {
enable = lib.mkEnableOption "Enable syncthing module";
};
};
config = lib.mkIf enabled {
services.syncthing = {
enable = true;
openDefaultPorts = true;
cert = config.age.secrets.syncthing-cert.path;
key = config.age.secrets.syncthing-key.path;
user = "alex";
group = "users";
dataDir = "/home/alex/sync";
guiAddress = "0.0.0.0:8384";
settings = {
gui = {
user = "syncthing";
password = "$2a$12$J/h/JOUiW24ZXsLYLEl2kOZUS1LftxANi0OlZxLy8Dst3/jpBd0v2";
insecureSkipHostcheck = false;
};
devices = {
phone.id = config.lib.syncthing.phone;
pinwheel.id = config.lib.syncthing.pinwheel;
tablet.id = config.lib.syncthing.tablet;
};
folders = {
org = {
path = "/home/alex/sync/org";
devices = [
"phone"
"pinwheel"
];
versioning = {
type = "staggered";
params = {
maxage = "2592000"; # 30 days
};
};
};
personal = {
path = "/home/alex/sync/personal";
devices = [ "pinwheel" ];
versioning = {
type = "staggered";
params = {
maxAge = "2592000"; # 30 days
};
};
};
work = {
path = "/home/alex/sync/work";
devices = [ "pinwheel" ];
versioning = {
type = "staggered";
params = {
maxAge = "2592000"; # 30 days
};
};
};
books = {
path = "/home/alex/sync/reading-material/books";
devices = [ "pinwheel" ];
versioning = {
type = "staggered";
params = {
maxAge = "2592000"; # 30 days
};
};
};
ttrpg = {
path = "/home/alex/sync/reading-material/ttrpg";
devices = [
"phone"
"tablet"
"pinwheel"
];
versioning = {
type = "staggered";
params = {
maxAge = "2592000"; # 30 days
};
};
};
"phone-gps" = {
path = "/home/alex/sync/phone-gps";
devices = [ "phone" ];
versioning = {
type = "staggered";
params = {
maxage = "2592000"; # 30 days
};
};
};
};
};
};
age = {
secrets = {
"syncthing-cert".file = ../../../../secrets/backwards/syncthing-cert.age;
"syncthing-key".file = ../../../../secrets/backwards/syncthing-key.age;
};
};
};
}
-2
View File
@@ -23,8 +23,6 @@
];
};
environment.variables.EDITOR = "vim";
environment.systemPackages = with pkgs; [
vim
git
-45
View File
@@ -79,16 +79,11 @@
{ device = config.disko.devices.disk.root.device; }
{ device = config.disko.devices.disk.disk1.device; }
{ device = config.disko.devices.disk.disk2.device; }
{ device = config.disko.devices.disk.disk3.device; }
{ device = config.disko.devices.disk.disk4.device; }
];
};
services.zfs.autoScrub.enable = true;
# Don't force-import the pool if it appears in use elsewhere; safer default in 26.11+.
boot.zfs.forceImportRoot = false;
networking.hostId = "0a9474e7"; # Required by ZFS
disko.devices = {
disk = {
@@ -154,39 +149,6 @@
};
};
};
disk3 = {
type = "disk";
device = "/dev/disk/by-id/ata-TOSHIBA_MG10ACA20TE_85K2A0UCF4MJ";
content = {
type = "gpt";
partitions = {
zfs = {
size = "100%";
content = {
type = "zfs";
pool = "storage";
};
};
};
};
};
disk4 = {
type = "disk";
device = "/dev/disk/by-id/ata-TOSHIBA_MG10ACA20TE_85K2A0V6F4MJ";
content = {
type = "gpt";
partitions = {
zfs = {
size = "100%";
content = {
type = "zfs";
pool = "storage";
};
};
};
};
};
};
zpool = {
@@ -204,13 +166,6 @@
"disk2"
];
}
{
mode = "mirror";
members = [
"disk3"
"disk4"
];
}
];
};
};
-7
View File
@@ -26,13 +26,6 @@
boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ];
swapDevices = [
{
device = "/swapfile";
size = 32 * 1024; # 32GB
}
];
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
+2 -6
View File
@@ -1,4 +1,4 @@
{ inputs, pkgs, ... }:
{ inputs, ... }:
{
imports = [ inputs.home-manager.nixosModules.home-manager ];
@@ -13,11 +13,7 @@
home.username = "alex";
home.homeDirectory = "/home/alex";
home.packages = [
pkgs.streamrip
pkgs.claude-code
pkgs.wl-clipboard
];
home.packages = [ ];
home.stateVersion = "24.11";
};
+1 -1
View File
@@ -8,7 +8,7 @@
};
environment.systemPackages = [
inputs.agenix.packages."${pkgs.stdenv.hostPlatform.system}".default
inputs.agenix.packages."${pkgs.system}".default
];
};
}
@@ -10,14 +10,6 @@ in
};
config = lib.mkIf enabled {
mod.homepage.services = [
{
name = "Audiobookshelf";
port = 8000;
description = "Audiobooks & podcasts";
}
];
users.users.audiobookshelf = {
isSystemUser = true;
description = "audiobookshelf";
@@ -0,0 +1,32 @@
{ lib, config, ... }:
let
enabled = config.mod.calibre-web.enable;
in
{
options = {
mod.calibre-web = {
enable = lib.mkEnableOption "add calibre-web module";
};
};
config = lib.mkIf enabled {
services = {
calibre-web = {
enable = true;
user = "storage";
group = "storage";
listen = {
ip = "0.0.0.0";
port = 8083;
};
options = {
calibreLibrary = "/mnt/media/public/books";
enableBookUploading = true;
};
};
};
};
}
-83
View File
@@ -1,83 +0,0 @@
{ config, ... }:
{
security.acme = {
acceptTerms = true;
defaults = {
email = "acme@ppp.pm";
};
certs = {
"ha.ppp.pm" = {
dnsProvider = "hetzner";
environmentFile = config.age.secrets.hetzner-dns.path;
group = "nginx";
extraLegoFlags = [
"--dns.resolvers=1.1.1.1:53,8.8.8.8:53"
"--dns.propagation-wait=60s" # Wait for 60 seconds for DNS propagation
"--dns-timeout=60"
"--http-timeout=60"
];
};
"komga.ppp.pm" = {
dnsProvider = "hetzner";
environmentFile = config.age.secrets.hetzner-dns.path;
group = "nginx";
extraLegoFlags = [
"--dns.resolvers=1.1.1.1:53,8.8.8.8:53"
"--dns.propagation-wait=60s"
"--dns-timeout=60"
"--http-timeout=60"
];
};
"romm.ppp.pm" = {
dnsProvider = "hetzner";
environmentFile = config.age.secrets.hetzner-dns.path;
group = "nginx";
extraLegoFlags = [
"--dns.resolvers=1.1.1.1:53,8.8.8.8:53"
"--dns.propagation-wait=60s"
"--dns-timeout=60"
"--http-timeout=60"
];
};
"puppy.ppp.pm" = {
dnsProvider = "hetzner";
environmentFile = config.age.secrets.hetzner-dns.path;
group = "nginx";
extraLegoFlags = [
"--dns.resolvers=1.1.1.1:53,8.8.8.8:53"
"--dns.propagation-wait=60s"
"--dns-timeout=60"
"--http-timeout=60"
];
};
"todo.ppp.pm" = {
dnsProvider = "hetzner";
environmentFile = config.age.secrets.hetzner-dns.path;
group = "nginx";
extraLegoFlags = [
"--dns.resolvers=1.1.1.1:53,8.8.8.8:53"
"--dns.propagation-wait=60s"
"--dns-timeout=60"
"--http-timeout=60"
];
};
};
};
age = {
secrets = {
"hetzner-dns".file = ../../../../secrets/manatee/hetzner-dns.age;
};
};
}
+2 -11
View File
@@ -12,23 +12,14 @@ in
ssh.enable = true;
git.enable = true;
tmux.enable = true;
nginx.enable = true;
syncthing.enable = true;
restic.enable = true;
transmission.enable = true;
calibre-web.enable = true;
audiobookshelf.enable = true;
jellyfin.enable = true;
immich.enable = false;
navidrome.enable = true;
komga.enable = true;
romm.enable = true;
homepage.enable = true;
disk-smart.enable = true;
puppy-tracker.enable = true;
solo-referee.enable = true;
todo.enable = true;
immich.enable = true;
};
};
}
@@ -1,159 +0,0 @@
{
pkgs,
lib,
config,
...
}:
let
enabled = config.mod.disk-smart.enable;
disks = [
{ path = "/dev/disk/by-id/ata-ST8000VN004-3CP101_WWZ8QCG4"; name = "seagate_8tb_1"; label = "Seagate 8TB #1"; }
{ path = "/dev/disk/by-id/ata-ST8000VN004-3CP101_WWZ8QDJ5"; name = "seagate_8tb_2"; label = "Seagate 8TB #2"; }
{ path = "/dev/disk/by-id/ata-TOSHIBA_MG10ACA20TE_85K2A0UCF4MJ"; name = "toshiba_20tb_1"; label = "Toshiba 20TB #1"; }
{ path = "/dev/disk/by-id/ata-TOSHIBA_MG10ACA20TE_85K2A0V6F4MJ"; name = "toshiba_20tb_2"; label = "Toshiba 20TB #2"; }
];
outputDir = "/var/lib/disk-smart";
collectScript = pkgs.writeShellScript "disk-smart-collect" ''
set -euo pipefail
export PATH="${lib.makeBinPath [ pkgs.smartmontools pkgs.jq pkgs.coreutils ]}"
mkdir -p ${outputDir}
result="{"
${lib.concatMapStringsSep "\n" (disk: ''
raw=$(smartctl -j -A -H ${disk.path} 2>/dev/null || true)
temp=$(echo "$raw" | jq -r '.temperature.current // empty')
power_on=$(echo "$raw" | jq -r '.power_on_time.hours // empty')
smart_status=$(echo "$raw" | jq -r '.smart_status.passed // empty')
reallocated=$(echo "$raw" | jq -r '[.ata_smart_attributes.table[] | select(.name == "Reallocated_Sector_Ct")][0].raw.value // empty')
pending=$(echo "$raw" | jq -r '[.ata_smart_attributes.table[] | select(.name == "Current_Pending_Sector")][0].raw.value // empty')
result="$result\"${disk.name}\":{\"temperature\":$temp,\"power_on_hours\":$power_on,\"smart_passed\":$smart_status,\"reallocated_sectors\":$reallocated,\"pending_sectors\":$pending},"
'') disks}
# Remove trailing comma, close object
result="''${result%,}}"
echo "$result" | jq . > ${outputDir}/smart.json.tmp
mv ${outputDir}/smart.json.tmp ${outputDir}/smart.json
'';
indent = prefix: s:
lib.concatMapStringsSep "\n"
(line: if line == "" then line else prefix + line)
(lib.splitString "\n" s);
mkSensor = disk: ''
- name: "${disk.label} Temperature"
value_template: "{{ value_json.${disk.name}.temperature }}"
unit_of_measurement: "°C"
device_class: temperature
state_class: measurement
- name: "${disk.label} Power On Hours"
value_template: "{{ value_json.${disk.name}.power_on_hours }}"
unit_of_measurement: "h"
state_class: total_increasing
- name: "${disk.label} SMART Passed"
value_template: "{{ value_json.${disk.name}.smart_passed }}"
- name: "${disk.label} Reallocated Sectors"
value_template: "{{ value_json.${disk.name}.reallocated_sectors }}"
state_class: measurement
- name: "${disk.label} Pending Sectors"
value_template: "{{ value_json.${disk.name}.pending_sectors }}"
state_class: measurement
'';
sensorYaml = indent " " (lib.concatMapStrings mkSensor disks);
sectorEntities = lib.concatMap (disk: [
"sensor.${disk.name}_reallocated_sectors"
"sensor.${disk.name}_pending_sectors"
]) disks;
sectorEntitiesYaml = lib.concatMapStringsSep "\n"
(id: " - ${id}") sectorEntities;
smartPassedEntities = map (disk: "sensor.${disk.name}_smart_passed") disks;
smartPassedEntitiesYaml = lib.concatMapStringsSep "\n"
(id: " - ${id}") smartPassedEntities;
in
{
options = {
mod.disk-smart = {
enable = lib.mkEnableOption "Enable disk SMART monitoring module";
};
};
config = lib.mkIf enabled {
mod.home-assistant.extraConfig = ''
rest:
- resource: http://127.0.0.1:9633/smart.json
scan_interval: 60
sensor:
${sensorYaml}
automation disk_smart:
- alias: "Disk sector count increased"
trigger:
- platform: state
entity_id:
${sectorEntitiesYaml}
condition:
- condition: template
value_template: "{{ trigger.from_state.state | int(-1) >= 0 and trigger.to_state.state | int(0) > trigger.from_state.state | int(0) }}"
action:
- service: notify.mobile_app_pixel_9_pro
data:
title: "Disk SMART warning"
message: "{{ trigger.to_state.attributes.friendly_name }} increased from {{ trigger.from_state.state }} to {{ trigger.to_state.state }}"
- alias: "Disk SMART check failed"
trigger:
- platform: state
entity_id:
${smartPassedEntitiesYaml}
condition:
- condition: template
value_template: "{{ trigger.to_state.state | lower == 'false' }}"
action:
- service: notify.mobile_app_pixel_9_pro
data:
title: "Disk SMART FAILURE"
message: "{{ trigger.to_state.attributes.friendly_name }} reports SMART failure drive is likely failing"
'';
systemd.services.disk-smart-collect = {
description = "Collect disk SMART data";
serviceConfig = {
Type = "oneshot";
ExecStart = collectScript;
};
};
systemd.timers.disk-smart-collect = {
description = "Periodically collect disk SMART data";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "1min";
OnUnitActiveSec = "1min";
};
};
services.nginx.virtualHosts."127.0.0.1" = {
listen = [
{ addr = "127.0.0.1"; port = 9633; }
];
locations."= /smart.json" = {
alias = "${outputDir}/smart.json";
extraConfig = ''
default_type application/json;
'';
};
};
};
}
+1 -1
View File
@@ -23,7 +23,7 @@ in
{ path = ./gitconfig; }
];
settings = {
extraConfig = {
rerere.enable = true;
};
};
@@ -1,300 +0,0 @@
{
pkgs,
lib,
config,
...
}:
let
nginxEnabled = config.mod.nginx.enable;
cfg = config.mod.home-assistant;
configFile = pkgs.writeText "ha-configuration.yaml" ''
# Loads default set of integrations. Do not remove.
default_config:
http:
use_x_forwarded_for: true
trusted_proxies:
- 127.0.0.1
# Load frontend themes from the themes folder
frontend:
themes: !include_dir_merge_named themes
automation: !include automations.yaml
script: !include scripts.yaml
scene: !include scenes.yaml
recorder:
purge_keep_days: 365
alert:
fridge_door:
name: Fridge is open
done_message: Fride is closed
entity_id: binary_sensor.kyldorr
state: "on"
repeat: 2
skip_first: true
notifiers:
- mobile_app_pixel_9_pro
${cfg.extraConfig}'';
btResetScript = pkgs.writeShellScript "bt-reset" ''
set -euo pipefail
export PATH="${
lib.makeBinPath [
pkgs.bluez
pkgs.util-linux
pkgs.kmod
pkgs.gnugrep
pkgs.coreutils
]
}"
logger -t bt-reset "Starting Bluetooth adapter reset..."
# Exit early if the adapter is already present and running
if hciconfig hci0 2>/dev/null | grep -q "UP RUNNING"; then
logger -t bt-reset "hci0 is already UP RUNNING nothing to do"
exit 0
fi
# If hci0 exists but isn't UP, try bringing it up
if hciconfig hci0 2>/dev/null; then
logger -t bt-reset "hci0 exists but not running bringing it up"
hciconfig hci0 up || true
sleep 2
if hciconfig hci0 2>/dev/null | grep -q "UP RUNNING"; then
logger -t bt-reset "hci0 is UP now"
systemctl restart bluetooth.service
logger -t bt-reset "bluetooth.service restarted done"
exit 0
fi
fi
# Hard reset: reload the btusb kernel module (works for USB adapters)
logger -t bt-reset "hci0 missing reloading btusb module..."
modprobe -r btusb 2>/dev/null || true
sleep 3
modprobe btusb
sleep 3
if hciconfig hci0 2>/dev/null; then
hciconfig hci0 up
logger -t bt-reset "hci0 restored after module reload"
else
logger -t bt-reset "ERROR: hci0 not found after module reload"
exit 1
fi
# Restart the bluetooth systemd service so bluetoothd picks up the adapter
systemctl restart bluetooth.service
logger -t bt-reset "bluetooth.service restarted done"
'';
in
{
options = {
mod.home-assistant = {
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Extra YAML to append to Home Assistant's configuration.yaml";
};
};
};
config = {
mod.homepage.services = [
{
name = "Home Assistant";
port = 8123;
description = "Home automation";
}
];
hardware.bluetooth.enable = true;
# Give up and enter a `failed` state (visible in `systemctl --failed`) if the
# container restarts more than 5 times in 5 minutes, instead of crash-looping
# forever. The window is deliberately long: a slow crash-loop (~5s per attempt)
# stays under systemd's default 5-starts-per-10s limit and would otherwise
# retry indefinitely.
systemd.services.podman-homeassistant = {
startLimitIntervalSec = 300;
startLimitBurst = 5;
};
virtualisation.oci-containers = {
backend = "podman";
containers.homeassistant = {
image = "ghcr.io/home-assistant/home-assistant:stable";
volumes = [
"/home/alex/.config/home-assistant:/config"
"${configFile}:/config/configuration.yaml:ro"
# Pass in bluetooth
"/run/dbus:/run/dbus:ro"
];
environment.TZ = "Europe/Stockholm";
extraOptions = [
"--network=host"
# Allows HA to perform low-level network operations (scan/reset adapter)
"--cap-add=NET_ADMIN"
"--cap-add=NET_RAW"
# Pass in Zigbee antenna
"--device=/dev/serial/by-id/usb-Nabu_Casa_ZBT-2_9C139EAAD464-if00:/dev/ttyACM0"
];
};
};
services = {
blueman.enable = true;
nginx = lib.mkIf nginxEnabled {
recommendedProxySettings = true;
virtualHosts."ha.ppp.pm" = {
forceSSL = true;
useACMEHost = "ha.ppp.pm";
extraConfig = ''
proxy_buffering off;
'';
locations."/" = {
proxyPass = "http://127.0.0.1:8123";
proxyWebsockets = true;
};
};
};
# Trigger reset via udev when hci0 disappears
udev.extraRules = ''
ACTION=="remove", SUBSYSTEM=="bluetooth", KERNEL=="hci0", \
TAG+="systemd", ENV{SYSTEMD_WANTS}+="bt-reset.service"
'';
};
systemd = {
services = {
# Trigger reset on bluetoothd failure
bluetooth = {
unitConfig.OnFailure = [ "bt-reset.service" ];
};
bt-reset = {
description = "Reset Bluetooth adapter";
after = [ "bluetooth.service" ];
serviceConfig = {
Type = "oneshot";
ExecStart = btResetScript;
Restart = "on-failure";
RestartSec = "10s";
StartLimitIntervalSec = "120";
StartLimitBurst = 3;
};
};
};
timers.bt-reset = {
description = "Periodically reset Bluetooth adapter";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "5min"; # first run 5 min after boot
OnUnitActiveSec = "4h"; # then every 4 hours
RandomizedDelaySec = "5min";
};
};
user = {
timers = {
"update-hetzner-dns" = {
unitConfig = {
Description = "updates Hetzner DNS records";
};
timerConfig = {
Unit = "update-hetzner-dns.service";
OnCalendar = "*-*-* *:00/30:00";
Persistent = true;
};
wantedBy = [ "timers.target" ];
};
};
services = {
"update-hetzner-dns" = {
unitConfig = {
Description = "updates Hetzner DNS records";
};
serviceConfig = {
Type = "exec";
EnvironmentFile = config.age.secrets.hetzner-dns.path;
};
path = [
pkgs.curl
pkgs.coreutils
pkgs.jq
];
script = ''
SUBDOMAINS="ha komga romm puppy todo"
INTERFACE="enp3s0"
CURRENT_IP=$(curl -s --fail --interface "$INTERFACE" ifconfig.me)
for SUBDOMAIN in $SUBDOMAINS; do
LAST_IP_FILE="/tmp/hetzner-dns-''${SUBDOMAIN}-ip"
LAST_IP=""
if [[ -f "$LAST_IP_FILE" ]]; then
LAST_IP=$(cat "$LAST_IP_FILE")
fi
if [[ "$CURRENT_IP" == "$LAST_IP" ]]; then
echo "$SUBDOMAIN: IP unchanged, NOOP update."
else
echo "$SUBDOMAIN: Updating IP"
JSON_BODY=$(jq -n --arg ip "$CURRENT_IP" '{records: [{value: $ip}]}')
curl \
--fail \
-X POST \
-H "Authorization: Bearer $HETZNER_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$JSON_BODY" \
"https://api.hetzner.cloud/v1/zones/ppp.pm/rrsets/''${SUBDOMAIN}/A/actions/set_records" \
&& echo $CURRENT_IP > $LAST_IP_FILE
fi
done
'';
};
};
};
};
age = {
secrets = {
"hetzner-dns" = {
file = ../../../../secrets/manatee/hetzner-dns.age;
owner = "alex";
group = "users";
};
};
};
};
}
-121
View File
@@ -1,121 +0,0 @@
{
pkgs,
lib,
config,
...
}:
let
enabled = config.mod.homepage.enable;
nginxEnabled = config.mod.nginx.enable;
services = config.mod.homepage.services;
serviceToCard =
svc:
let
href = if svc.url != null then svc.url else "http://manatee:${toString svc.port}";
in
''
<a class="card" href="${href}">
<div class="name">${svc.name}</div>
<div class="desc">${svc.description}</div>
<div class="port">:${toString svc.port}</div>
</a>
'';
page = pkgs.writeTextDir "index.html" ''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>manatee</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #1a1b26;
color: #c0caf5;
min-height: 100vh;
padding: 3rem 1.5rem;
}
h1 {
text-align: center;
font-size: 1.5rem;
font-weight: 400;
color: #7aa2f7;
margin-bottom: 2rem;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
max-width: 900px;
margin: 0 auto;
}
.card {
display: block;
background: #24283b;
border: 1px solid #414868;
border-radius: 8px;
padding: 1.25rem;
text-decoration: none;
color: inherit;
transition: border-color 0.15s;
}
.card:hover { border-color: #7aa2f7; }
.name { font-size: 1.1rem; font-weight: 600; color: #c0caf5; }
.desc { font-size: 0.85rem; color: #565f89; margin-top: 0.35rem; }
.port { font-size: 0.8rem; color: #414868; margin-top: 0.5rem; font-family: monospace; }
</style>
</head>
<body>
<h1>manatee</h1>
<div class="grid">
${lib.concatMapStrings serviceToCard services}
</div>
</body>
</html>
'';
in
{
options = {
mod.homepage = {
enable = lib.mkEnableOption "Enable homepage module";
services = lib.mkOption {
type = lib.types.listOf (
lib.types.submodule {
options = {
name = lib.mkOption { type = lib.types.str; };
port = lib.mkOption { type = lib.types.port; };
description = lib.mkOption { type = lib.types.str; };
url = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Link target for the card; defaults to http://manatee:<port>.";
};
};
}
);
default = [ ];
description = "Services to display on the homepage";
};
};
};
config = lib.mkIf (enabled && nginxEnabled) {
services.nginx.virtualHosts."homepage" = {
listen = [
{
addr = "0.0.0.0";
port = 9999;
}
];
root = page;
locations."/" = {
index = "index.html";
};
};
networking.firewall.allowedTCPPorts = [ 9999 ];
};
}
-8
View File
@@ -10,14 +10,6 @@ in
};
config = lib.mkIf enabled {
mod.homepage.services = [
{
name = "Immich";
port = 2283;
description = "Photo library";
}
];
users.users.immich = {
isSystemUser = true;
group = "storage";
@@ -31,7 +31,6 @@ in
extraPackages = [
pkgs.intel-media-driver # Modern Intel VA-API driver (needed for N305)
pkgs.libvdpau-va-gl # VDPAU backend for VA-API GLX interop
pkgs.intel-compute-runtime # OpenCL support
];
};
@@ -47,18 +46,6 @@ in
};
};
mod.homepage.services = [
{
name = "Jellyfin";
port = 8096;
description = "Media streaming";
}
];
networking = {
firewall.allowedTCPPorts = [ 8096 ];
};
environment.systemPackages = [
pkgs.jellyfin
pkgs.jellyfin-web
-153
View File
@@ -1,153 +0,0 @@
{
inputs,
pkgs,
lib,
config,
...
}:
let
enabled = config.mod.komga.enable;
nginxEnabled = config.mod.nginx.enable;
in
{
options = {
mod.komga = {
enable = lib.mkEnableOption "Enable komga module";
};
};
imports = [
inputs.komga-comictracker.nixosModules.default
inputs.komga-bookmanager.nixosModules.default
inputs.komga-reading-stats.nixosModules.default
];
config = lib.mkIf enabled {
mod.homepage.services = [
{
name = "Komga";
port = 8002;
description = "Comic library";
}
{
name = "Komga Reader";
port = 8888;
description = "Comic reader";
}
{
name = "Komga Book Manager";
port = 8686;
description = "Book manager";
}
{
name = "Komga Reading Stats";
port = 8787;
description = "Reading stats";
}
];
users.users.komga = {
isSystemUser = true;
group = "storage";
};
services.komga = {
enable = true;
user = "komga";
group = "storage";
settings = {
server.port = 8002;
komga."cors.allowed-origins" = [
"http://manatee:8888"
"https://komga.ppp.pm"
];
};
openFirewall = true;
};
services.nginx = lib.mkIf nginxEnabled {
virtualHosts."komga-reader" = {
listen = [
{
addr = "0.0.0.0";
port = 8888;
}
];
root = (pkgs.writeTextDir "komga-reader.html" (builtins.readFile ./komga-reader.html));
locations."/" = {
index = "komga-reader.html";
tryFiles = "$uri $uri/ /komga-reader.html";
};
};
virtualHosts."komga.ppp.pm" = {
forceSSL = true;
useACMEHost = "komga.ppp.pm";
locations."/" = {
proxyPass = "http://127.0.0.1:8002";
proxyWebsockets = true;
};
};
};
networking.firewall.allowedTCPPorts = [ 8888 ];
programs.comictracker = {
enable = true;
komgaUrl = "http://127.0.0.1:8002";
komgaLibraryId = "0NVZH5AK3RPE1";
secretsFile = config.age.secrets.komga-comicbooktracker-credentials.path;
};
services.komga-book-manager = {
enable = true;
port = 8686;
group = "storage";
komgaUrl = "http://127.0.0.1:8002";
credentialsFile = config.age.secrets.komga-bookmanager-credentials.path;
libraryRoot = "/mnt/media/public/books";
libraryId = "0PNE1NEPY6995";
};
services.komga-reading-stats = {
enable = true;
host = "0.0.0.0";
port = 8787;
origin = "http://manatee:8787";
anthropicApiKeyFile = config.age.secrets.komga-reading-stats-claude-api-key.path;
claudeModel = "claude-sonnet-4-6";
komga = {
url = "http://127.0.0.1:8002";
apiKeyFile = config.age.secrets.komga-reading-stats-komga-api-key.path;
};
};
age.secrets = {
"komga-comicbooktracker-credentials" = {
file = ../../../../secrets/manatee/komga-comicbooktracker-credentials.age;
owner = "alex";
group = "users";
};
"komga-bookmanager-credentials" = {
file = ../../../../secrets/manatee/komga-bookmanager-credentials.age;
owner = "alex";
group = "users";
};
"komga-reading-stats-claude-api-key" = {
file = ../../../../secrets/manatee/komga-reading-stats-claude-api-key.age;
};
"komga-reading-stats-komga-api-key" = {
file = ../../../../secrets/manatee/komga-reading-stats-komga-api-key.age;
};
};
};
}
File diff suppressed because it is too large Load Diff
@@ -1,41 +0,0 @@
{
lib,
pkgs,
config,
...
}:
let
navidromeEnabled = config.mod.navidrome.enable;
in
{
options = {
mod.navidrome = {
enable = lib.mkEnableOption "Enable navidrome module";
};
};
config = {
mod.homepage.services = lib.mkIf navidromeEnabled [
{
name = "Navidrome";
port = 4533;
description = "Music streaming";
}
];
services = lib.mkIf navidromeEnabled {
navidrome = {
enable = true;
openFirewall = true;
user = "navidrome";
group = "storage";
settings = {
Port = 4533;
Address = "0.0.0.0";
MusicFolder = "/mnt/media/public/music";
};
};
};
};
}
+1 -29
View File
@@ -1,15 +1,8 @@
{ ... }:
let
hostAddress = "192.168.50.203";
in
{
networking = {
hostName = "manatee";
# Required for asymmetric routing (sending replies out a different interface
# than the default route). Without this, the kernel drops the return traffic.
firewall.checkReversePath = "loose";
defaultGateway = "192.168.50.1";
nameservers = [ "1.1.1.1" ];
interfaces = {
@@ -18,33 +11,12 @@ in
ipv4 = {
addresses = [
{
address = hostAddress;
address = "192.168.50.203";
prefixLength = 24;
}
];
};
ipv4.routes = [
{
address = "0.0.0.0";
prefixLength = 0;
via = "192.168.50.1"; # Router
options = {
table = "100";
};
}
];
};
};
localCommands = ''
# Ensure local LAN traffic uses the main table, e.g. responds to the local machine
ip rule list | grep -q "192.168.50.0/24 lookup main" || \
ip rule add to 192.168.50.0/24 lookup main priority 4999
# All other traffic from this IP uses Table 100 (e.g. responds to router and back out)
ip rule list | grep -q "from ${hostAddress} lookup 100" || \
ip rule add from ${hostAddress} lookup 100 priority 5000
'';
};
}
-6
View File
@@ -18,11 +18,5 @@ in
recommendedTlsSettings = true;
};
};
networking = {
firewall = {
allowedTCPPorts = [ 443 ];
};
};
};
}
@@ -1,63 +0,0 @@
{
inputs,
lib,
config,
...
}:
let
enabled = config.mod.puppy-tracker.enable;
nginxEnabled = config.mod.nginx.enable;
port = 8089;
in
{
options = {
mod.puppy-tracker = {
enable = lib.mkEnableOption "Enable puppy-tracker module";
};
};
imports = [
inputs.puppy-tracker.nixosModules.default
];
config = lib.mkIf enabled {
mod.homepage.services = [
{
name = "Puppy Tracker";
port = port;
description = "Sleep, meals, pees, poos";
# Login needs HTTPS (Secure cookies), so link to the public vhost.
url = "https://puppy.ppp.pm";
}
];
services.puppy-tracker = {
enable = true;
inherit port;
openFirewall = true;
# Served publicly over HTTPS via the nginx vhost below.
secureCookies = true;
# Shared registration secret; the file holds `PUPPY_INVITE_CODE=...`.
inviteCodeFile = config.age.secrets."puppy-tracker-invite-code".path;
};
services.nginx = lib.mkIf nginxEnabled {
virtualHosts."puppy.ppp.pm" = {
forceSSL = true;
useACMEHost = "puppy.ppp.pm";
# Photo uploads are up to 15 MB; give nginx headroom over its 1 MB default.
extraConfig = ''
client_max_body_size 20m;
'';
locations."/" = {
proxyPass = "http://127.0.0.1:${toString port}";
};
};
};
age.secrets."puppy-tracker-invite-code".file =
../../../../secrets/manatee/puppy-tracker-invite-code.age;
};
}
-210
View File
@@ -1,210 +0,0 @@
{
pkgs,
lib,
config,
...
}:
let
enabled = config.mod.restic.enable;
# Where the consistent, restic-ready copies of service state are staged.
# Lives on the external backup disk. root-only (0700): these dumps contain
# credentials (HA .storage, secrets.yaml, session tokens, the romm database).
stateDir = "/mnt/backup/public/state";
# Produce consistent, restore-ready copies of every service's state under
# ${stateDir}. Runs as root before the state backups. SQLite DBs are copied
# via the online-backup API (safe on a live, WAL-mode DB); the romm MariaDB is
# dumped logically with mariadb-dump --single-transaction. Rebuildable caches
# and indexes are skipped to keep the offsite copy lean.
stateDumpScript = pkgs.writeShellScript "state-backup-dump" ''
export PATH=${
lib.makeBinPath [
pkgs.sqlite
pkgs.rsync
pkgs.podman
pkgs.coreutils
pkgs.findutils
]
}
set -euo pipefail
umask 077
STATE=${lib.escapeShellArg stateDir}
install -d -m700 "$STATE"
# snapshot SRC DEST [extra rsync excludes...]
# Copies plain files with rsync (excluding live DBs, their WAL/SHM sidecars
# and logs), then makes a consistent online copy of each *.db / *.sqlite.
snapshot() {
local src="$1" dst="$2"
shift 2
if [ ! -e "$src" ]; then
echo "state-backup: skip (missing) $src"
return 0
fi
install -d -m700 "$dst"
rsync -a --delete \
--exclude='*.sqlite' --exclude='*.sqlite-shm' --exclude='*.sqlite-wal' \
--exclude='*.db' --exclude='*.db-shm' --exclude='*.db-wal' \
--exclude='*.log' --exclude='*.log.*' \
--exclude='__pycache__/' \
"$@" \
"$src/" "$dst/"
while IFS= read -r db; do
local rel="''${db#"$src"/}"
install -d -m700 "$dst/$(dirname "$rel")"
sqlite3 "$db" ".backup '$dst/$rel'"
done < <(find "$src" -maxdepth 4 -type f \( -name '*.sqlite' -o -name '*.db' \))
}
# --- romm: logical MariaDB dump + user assets (saves/states/screenshots) ---
# Cover art under resources/ is intentionally skipped (refetched from
# metadata providers); redis is a rebuildable cache.
if podman exec romm-db true 2>/dev/null; then
install -d -m700 "$STATE/romm"
podman exec romm-db sh -c \
'exec mariadb-dump --user="$MARIADB_USER" --password="$MARIADB_PASSWORD" --single-transaction --no-tablespaces "$MARIADB_DATABASE"' \
> "$STATE/romm/romm.sql"
rsync -a --delete /var/lib/romm/assets/ "$STATE/romm/assets/"
else
echo "state-backup: skip (romm-db not running)"
fi
# --- komga: only database.sqlite matters (read progress, users, collections).
# tasks.sqlite is transient and the lucene/ index is rebuilt on demand. ---
if [ -e /var/lib/komga/database.sqlite ]; then
install -d -m700 "$STATE/komga"
sqlite3 /var/lib/komga/database.sqlite ".backup '$STATE/komga/database.sqlite'"
fi
# --- home-assistant: config, credentials and integrations, plus the two live
# SQLite DBs. Recorder history (home-assistant_v2.db) is included per choice.
# deps/ and tts/ are reinstalled/regenerated; logs are dropped. ---
HA=/home/alex/.config/home-assistant
if [ -e "$HA" ]; then
install -d -m700 "$STATE/home-assistant"
rsync -a --delete \
--exclude='home-assistant_v2.db' --exclude='home-assistant_v2.db-shm' --exclude='home-assistant_v2.db-wal' \
--exclude='zigbee.db' --exclude='zigbee.db-shm' --exclude='zigbee.db-wal' \
--exclude='*.log' --exclude='*.log.*' --exclude='*.log.fault' \
--exclude='deps/' --exclude='tts/' --exclude='__pycache__/' \
"$HA/" "$STATE/home-assistant/"
for db in home-assistant_v2.db zigbee.db; do
if [ -e "$HA/$db" ]; then
sqlite3 "$HA/$db" ".backup '$STATE/home-assistant/$db'"
fi
done
fi
# --- remaining NixOS services: generic snapshot, skipping rebuildable caches ---
snapshot /var/lib/navidrome "$STATE/navidrome" --exclude='cache/'
snapshot /var/lib/jellyfin "$STATE/jellyfin" --exclude='transcodes/' --exclude='cache/' --exclude='metadata/' --exclude='log/' --exclude='data/subtitles/'
snapshot /var/lib/audiobookshelf "$STATE/audiobookshelf" --exclude='metadata/'
snapshot /var/lib/puppy-tracker "$STATE/puppy-tracker"
snapshot /var/lib/solo-referee "$STATE/solo-referee"
snapshot /var/lib/todo "$STATE/todo"
snapshot /var/lib/komga-book-manager "$STATE/komga-book-manager"
snapshot /var/lib/komga-reading-stats "$STATE/komga-reading-stats"
echo "state-backup: dump complete"
'';
# Retention shared by all jobs.
pruneOpts = [
"--keep-daily 1"
"--keep-weekly 7"
"--keep-yearly 12"
];
in
{
options.mod.restic.enable = lib.mkEnableOption "Enable restic";
config = lib.mkIf enabled {
fileSystems."/mnt/backup" = {
device = "/dev/disk/by-uuid/34601701-65e6-4b2c-ac4d-8bef3dfd743f";
fsType = "ext4";
options = [ "nofail" ];
};
services.restic.backups = {
"sync-to-external" = {
initialize = true;
passwordFile = config.age.secrets.restic-password.path;
paths = [ "/mnt/sync/public" ];
repository = "/mnt/backup/restic";
timerConfig = {
OnCalendar = "*-*-* 0/12:00:00";
Persistent = true;
};
inherit pruneOpts;
};
"sync-to-cloud" = {
initialize = true;
passwordFile = config.age.secrets.restic-password.path;
environmentFile = config.age.secrets.restic-cloud-sync-key.path;
repositoryFile = config.age.secrets.restic-cloud-sync-repository.path;
paths = [ "/mnt/sync/public" ];
timerConfig = {
OnCalendar = "*-*-* 0/12:00:00";
Persistent = true;
};
inherit pruneOpts;
};
# Service state (databases etc.). No timer of their own: the state-backup
# orchestrator below runs the dump once and then triggers these in order,
# so both back up the same consistent staging dir without racing it.
"state-to-external" = {
initialize = true;
passwordFile = config.age.secrets.restic-password.path;
paths = [ stateDir ];
repository = "/mnt/backup/restic";
timerConfig = null;
inherit pruneOpts;
};
"state-to-cloud" = {
initialize = true;
passwordFile = config.age.secrets.restic-password.path;
environmentFile = config.age.secrets.restic-cloud-sync-key.path;
repositoryFile = config.age.secrets.restic-cloud-sync-repository.path;
paths = [ stateDir ];
timerConfig = null;
inherit pruneOpts;
};
};
# Dump service state, then push it to the external disk and the cloud, in
# that order. A "-" prefix means a failure of one restic target does not
# block the other; a failed dump (no prefix) aborts before either runs.
systemd.services.state-backup = {
description = "Dump service state and back it up (external + cloud)";
after = [ "podman.service" ];
serviceConfig = {
Type = "oneshot";
ExecStart = [
"${stateDumpScript}"
"-${pkgs.systemd}/bin/systemctl start --wait restic-backups-state-to-external.service"
"-${pkgs.systemd}/bin/systemctl start --wait restic-backups-state-to-cloud.service"
];
};
};
systemd.timers.state-backup = {
description = "Timer for service-state backups";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "*-*-* 0/12:00:00";
Persistent = true;
};
};
age.secrets = {
"restic-password".file = ../../../../secrets/manatee/restic-password.age;
"restic-cloud-sync-key".file = ../../../../secrets/manatee/restic-cloud-sync-key.age;
"restic-cloud-sync-repository".file = ../../../../secrets/manatee/restic-cloud-sync-repository.age;
};
};
}
-230
View File
@@ -1,230 +0,0 @@
{
pkgs,
lib,
config,
...
}:
let
enabled = config.mod.romm.enable;
nginxEnabled = config.mod.nginx.enable;
configFile = pkgs.writeText "romm-config.yml" ''
filesystem:
skip_hash_calculation: false
exclude:
roms:
single_file:
extensions:
- xml
- txt
- nfo
- dat
- jpg
- png
names:
- '._*'
- 'Thumbs.db'
- '.DS_Store'
'';
# Give up and enter a `failed` state (visible in `systemctl --failed`) if a
# container restarts more than 5 times in 5 minutes, instead of crash-looping
# forever. The window is deliberately long: a slow crash-loop (~5s per attempt)
# stays under systemd's default 5-starts-per-10s limit and would otherwise
# retry indefinitely.
crashLoopGuard = {
startLimitIntervalSec = 300;
startLimitBurst = 5;
};
in
{
options = {
mod.romm = {
enable = lib.mkEnableOption "Enable romm module";
};
};
config = lib.mkIf enabled {
mod.homepage.services = [
{
name = "RomM";
port = 8085;
description = "ROM library manager";
}
];
systemd.tmpfiles.rules = [
"d /var/lib/romm 0755 root root -"
"d /var/lib/romm/db 0755 root root -"
"d /var/lib/romm/redis 0755 999 1000 -"
"d /var/lib/romm/resources 0755 root root -"
"d /var/lib/romm/assets 0755 root root -"
];
systemd.services.podman-romm = crashLoopGuard;
systemd.services.podman-romm-db = crashLoopGuard;
systemd.services.podman-romm-redis = crashLoopGuard;
systemd.services.romm-net = {
description = "Create Podman network for RomM";
after = [ "podman.service" ];
requires = [ "podman.service" ];
before = [
"podman-romm.service"
"podman-romm-db.service"
"podman-romm-redis.service"
];
requiredBy = [
"podman-romm.service"
"podman-romm-db.service"
"podman-romm-redis.service"
];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = pkgs.writeShellScript "romm-net-create" ''
${pkgs.podman}/bin/podman network exists romm-net \
|| ${pkgs.podman}/bin/podman network create romm-net
'';
ExecStop = "${pkgs.podman}/bin/podman network rm -f romm-net";
};
};
virtualisation.oci-containers = {
backend = "podman";
containers.romm-db = {
image = "mariadb:latest";
environment = {
MARIADB_DATABASE = "romm";
MARIADB_USER = "romm";
};
environmentFiles = [
config.age.secrets.romm-db-password.path
];
volumes = [
"/var/lib/romm/db:/var/lib/mysql"
];
extraOptions = [
"--network=romm-net"
];
};
containers.romm-redis = {
image = "redis:alpine";
volumes = [
"/var/lib/romm/redis:/data"
];
extraOptions = [
"--network=romm-net"
"--user=root"
];
};
containers.romm = {
image = "rommapp/romm:latest";
dependsOn = [
"romm-db"
"romm-redis"
];
environment = {
DB_HOST = "romm-db";
DB_PORT = "3306";
DB_NAME = "romm";
DB_USER = "romm";
REDIS_HOST = "romm-redis";
REDIS_PORT = "6379";
ROMM_AUTH_ENABLED = "true";
};
environmentFiles = [
config.age.secrets.romm-auth-secret-key.path
config.age.secrets.romm-db-password.path
config.age.secrets.romm-metadata-api-keys.path
];
ports = [
"127.0.0.1:8086:8080"
];
volumes = [
"${configFile}:/romm/config/config.yml:ro"
"/mnt/media/public/games:/romm/library"
"/var/lib/romm/resources:/romm/resources"
"/var/lib/romm/assets:/romm/assets"
];
extraOptions = [
"--network=romm-net"
];
};
};
services.nginx = lib.mkIf nginxEnabled {
virtualHosts."romm-local" = {
listen = [
{
addr = "0.0.0.0";
port = 8085;
}
];
extraConfig = ''
client_max_body_size 0;
'';
locations."/" = {
proxyPass = "http://127.0.0.1:8086";
proxyWebsockets = true;
};
};
virtualHosts."romm.ppp.pm" = {
forceSSL = true;
useACMEHost = "romm.ppp.pm";
extraConfig = ''
client_max_body_size 0;
'';
locations."/" = {
proxyPass = "http://127.0.0.1:8086";
proxyWebsockets = true;
};
};
};
boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
networking.firewall.allowedTCPPorts = [ 8085 ];
age.secrets = {
"romm-auth-secret-key" = {
file = ../../../../secrets/manatee/romm-auth-secret-key.age;
owner = "root";
group = "root";
};
"romm-db-password" = {
file = ../../../../secrets/manatee/romm-db-password.age;
owner = "root";
group = "root";
};
"romm-metadata-api-keys" = {
file = ../../../../secrets/manatee/romm-metadata-api-keys.age;
owner = "root";
group = "root";
};
};
};
}
@@ -1,46 +0,0 @@
{
inputs,
lib,
config,
...
}:
let
enabled = config.mod.solo-referee.enable;
port = 8090;
in
{
options = {
mod.solo-referee = {
enable = lib.mkEnableOption "Enable solo-referee module";
};
};
imports = [
inputs.solo-referee.nixosModules.default
];
config = lib.mkIf enabled {
mod.homepage.services = [
{
name = "Solo Referee";
port = port;
description = "Solo-RPG tool with Claude as GM";
# url omitted → homepage links to http://manatee:${port}.
}
];
services.solo-referee = {
enable = true;
inherit port;
# Reachable on the LAN / Tailscale at manatee:${port}; no public vhost.
address = "0.0.0.0";
openFirewall = true;
# Anthropic API key kept out of the store; the file holds
# `ANTHROPIC_API_KEY=...`. Without it the offline stub GM runs.
apiKeyFile = config.age.secrets."solo-referee-api-key".path;
};
age.secrets."solo-referee-api-key".file =
../../../../secrets/manatee/solo-referee-api-key.age;
};
}
+1 -15
View File
@@ -21,26 +21,12 @@ in
home-manager.users.alex = {
programs.ssh = {
enable = true;
enableDefaultConfig = false;
settings = {
matchBlocks = {
"git.ppp.pm" = {
hostname = "git.ppp.pm";
identityFile = "/home/alex/.ssh/alex.manatee-git.ppp.pm";
};
"*" = {
forwardAgent = false;
addKeysToAgent = "no";
compression = false;
serverAliveInterval = 0;
serverAliveCountMax = 3;
hashKnownHosts = false;
userKnownHostsFile = "~/.ssh/known_hosts";
controlMaster = "no";
controlPath = "~/.ssh/master-%r@%n:%p";
controlPersist = "no";
};
};
};
};
@@ -10,14 +10,6 @@ in
};
config = lib.mkIf enabled {
mod.homepage.services = [
{
name = "Syncthing";
port = 8384;
description = "File sync";
}
];
services.syncthing = {
enable = true;
@@ -39,7 +31,6 @@ in
};
devices = {
phone.id = config.lib.syncthing.phone;
pinwheel.id = config.lib.syncthing.pinwheel;
};
@@ -47,7 +38,6 @@ in
org = {
path = "/mnt/sync/public/org";
devices = [
"phone"
"pinwheel"
];
versioning = {
@@ -57,39 +47,6 @@ in
};
};
};
personal = {
path = "/mnt/sync/public/personal";
devices = [ "pinwheel" ];
versioning = {
type = "staggered";
params = {
maxage = "2592000"; # 30 days
};
};
};
work = {
path = "/mnt/sync/public/work";
devices = [ "pinwheel" ];
versioning = {
type = "staggered";
params = {
maxage = "2592000"; # 30 days
};
};
};
"phone-gps" = {
path = "/mnt/sync/public/phone-gps";
devices = [ "phone" ];
versioning = {
type = "staggered";
params = {
maxage = "2592000"; # 30 days
};
};
};
};
};
};
-71
View File
@@ -1,71 +0,0 @@
{
pkgs,
lib,
config,
...
}:
let
enabled = config.mod.tmux.enable;
in
{
options = {
mod.tmux = {
enable = lib.mkEnableOption "enable tmux module";
};
};
config = lib.mkIf enabled {
home-manager.users.alex = {
programs.tmux = {
enable = true;
baseIndex = 1;
keyMode = "vi";
# Allow vi mode to be enabled instantly
escapeTime = 0;
plugins = [ pkgs.tmuxPlugins.sensible ];
extraConfig = ''
set -g renumber-windows on
# https://old.reddit.com/r/tmux/comments/mesrci/tmux_2_doesnt_seem_to_use_256_colors/
set -g default-terminal "xterm-256color"
set -ga terminal-overrides ",*256col*:Tc"
set -ga terminal-overrides ',*:Ss=\E[%p1%d q:Se=\E[ q'
set-environment -g COLORTERM "truecolor"
set-option -g allow-rename off
# Remove date/time etc. on the right side
set -g status-right ""
bind r source-file ~/.config/tmux/tmux.conf \; display "Config reloaded"
# Remove accidental `suspend-client` triggers
unbind C-z
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
# Move panes shortcuts
bind h select-pane -L
bind j select-pane -D
bind k select-pane -U
bind l select-pane -R
# Resize panes
bind -r H resize-pane -L 5
bind -r J resize-pane -D 5
bind -r K resize-pane -U 5
bind -r L resize-pane -R 5
# Move windows
bind -r Left swap-window -t -1 \; select-window -t -1
bind -r Right swap-window -t +1 \; select-window -t +1
'';
};
};
};
}
-58
View File
@@ -1,58 +0,0 @@
{
inputs,
lib,
config,
...
}:
let
enabled = config.mod.todo.enable;
nginxEnabled = config.mod.nginx.enable;
port = 8091;
in
{
options = {
mod.todo = {
enable = lib.mkEnableOption "Enable todo module";
};
};
imports = [
inputs.todo.nixosModules.default
];
config = lib.mkIf enabled {
mod.homepage.services = [
{
name = "Todo";
port = port;
description = "Offline-first todo";
# Login needs HTTPS (Secure cookies), so link to the public vhost.
url = "https://todo.ppp.pm";
}
];
services.todo = {
enable = true;
address = "127.0.0.1";
inherit port;
# Served publicly over HTTPS via the nginx vhost below.
secureCookies = true;
tokenFile = config.age.secrets."todo-token".path;
# sessionSecretFile is left unset: the app generates one in its state dir
# (/var/lib/todo) on first boot, mode 0600.
};
services.nginx = lib.mkIf nginxEnabled {
virtualHosts."todo.ppp.pm" = {
forceSSL = true;
useACMEHost = "todo.ppp.pm";
locations."/" = {
proxyPass = "http://127.0.0.1:${toString port}";
};
};
};
age.secrets."todo-token".file = ../../../../secrets/manatee/todo-token.age;
};
}
@@ -15,14 +15,6 @@ in
};
config = lib.mkIf enabled {
mod.homepage.services = [
{
name = "Transmission";
port = 9091;
description = "Torrent client";
}
];
services = {
transmission = {
enable = true;
+1 -4
View File
@@ -17,10 +17,7 @@
users.users.alex = {
isNormalUser = true;
description = "alex";
extraGroups = [
"wheel"
"networkmanager"
];
extraGroups = [ "wheel" ];
};
environment.systemPackages = with pkgs; [
+4 -6
View File
@@ -14,19 +14,17 @@
home.homeDirectory = "/home/alex";
home.packages = [
inputs.whib-backend.packages.${pkgs.stdenv.hostPlatform.system}.whib-import
# pkgs.beekeeper-studio
inputs.whib-backend.packages.${pkgs.system}.whib-import
pkgs.beekeeper-studio
pkgs.bitwarden-desktop
pkgs.gimp
pkgs.zip
pkgs.unzip
pkgs.unar
pkgs.jq
pkgs.dbeaver-bin
pkgs.htop
pkgs.onlyoffice-desktopeditors
pkgs.onlyoffice-bin
pkgs.wdisplays
pkgs.vlc
pkgs.opencode
];
home.stateVersion = "23.05";
+1 -1
View File
@@ -11,7 +11,7 @@
};
environment.systemPackages = [
inputs.agenix.packages."${pkgs.stdenv.hostPlatform.system}".default
inputs.agenix.packages."${pkgs.system}".default
];
};
}
-67
View File
@@ -1,67 +0,0 @@
{
pkgs,
lib,
config,
...
}:
let
enabled = config.mod.ai.enable;
# Shared statusline: show the logged-in account email.
statusLine = {
type = "command";
command = "jq -r '.oauthAccount.emailAddress' \"$CLAUDE_CONFIG_DIR/.claude.json\"";
};
# PostToolUse hook: echoes a JSON payload that injects a comment-policy
# reminder as additional context after every Write/Edit.
commentPolicyPayload = builtins.toJSON {
suppressOutput = true;
hookSpecificOutput = {
hookEventName = "PostToolUse";
additionalContext = "Comment policy: default to ZERO code comments. Review what you just wrote and delete any comment that narrates the change, restates the code, states the obvious, is tangential, or explains design/why rationale -- put that reasoning in your chat reply instead, never in the code. Never use em dashes or en dashes anywhere in file content; use a plain ASCII hyphen or reword.";
};
};
settings = builtins.toJSON {
inherit statusLine;
theme = "dark";
editorMode = "vim";
hooks = {
PostToolUse = [
{
matcher = "Write|Edit";
hooks = [
{
type = "command";
command = "echo ${lib.escapeShellArg commentPolicyPayload}";
}
];
}
];
};
};
in
{
options = {
mod.ai = {
enable = lib.mkEnableOption "enable ai module";
};
};
config = lib.mkIf enabled {
home-manager.users.alex = {
home.packages = [
pkgs.claude-code
];
programs.zsh.shellAliases = {
wclaude = "CLAUDE_CONFIG_DIR=$HOME/.claude-personal claude";
pclaude = "CLAUDE_CONFIG_DIR=$HOME/.claude-work claude";
};
home.file.".claude-work/settings.json".text = settings;
home.file.".claude-personal/settings.json".text = settings;
};
};
}
+1 -8
View File
@@ -28,14 +28,7 @@ in
wayland.windowManager.hyprland = lib.mkIf hyprlandEnabled {
settings = {
bind = [
{
_args = [
"SUPER + SPACE"
(lib.generators.mkLuaInline ''hl.dsp.exec_cmd("${bmr}")'')
];
}
];
bind = [ "$mod, SPACE, exec, ${bmr}" ];
};
};
};
@@ -30,6 +30,12 @@ in
# Low battery notification for bluetooth devices
systemd.user =
let
trackpad = {
id = "battery_hid_a8o91o3doe5ofeo38_battery";
name = "trackpad";
threshold = "20";
};
headphones = {
id = "headset_dev_38_18_4C_18_A4_6E";
name = "headphones";
@@ -59,6 +65,7 @@ in
in
builtins.listToAttrs (
builtins.map mkTimer [
trackpad
headphones
]
);
@@ -112,6 +119,7 @@ in
in
builtins.listToAttrs (
builtins.map mkService [
trackpad
headphones
]
);
+1 -4
View File
@@ -14,7 +14,7 @@ in
nix-index.enable = false;
greetd.enable = true;
hyprland.enable = true;
hyprlock.enable = true;
swaylock.enable = true;
physlock.enable = false;
power.enable = true;
@@ -33,7 +33,6 @@ in
rust.enable = true;
scala.enable = true;
python.enable = true;
gleam.enable = true;
keyboard.enable = true;
containers = {
@@ -42,8 +41,6 @@ in
};
vm.enable = true;
scripts.enable = true;
ai.enable = true;
};
};
}
+12 -51
View File
@@ -58,7 +58,7 @@
#+END_SRC
*** This makes emacsclient startup faster in TUI-mode
#+BEGIN_SRC emacs-lisp
(setq-default xterm-query-timeout 0.1)
(setq-default xterm-query-timeout nil)
#+END_SRC
*** Disable startup messages
#+BEGIN_SRC emacs-lisp
@@ -479,34 +479,7 @@ Setup prefix for keybindings.
* Flycheck
#+BEGIN_SRC emacs-lisp
(use-package flycheck
:preface
(defun mp-flycheck-eldoc (callback &rest _ignored)
"Print flycheck messages at point by calling CALLBACK."
(when-let ((flycheck-errors (and flycheck-mode (flycheck-overlay-errors-at (point)))))
(mapc
(lambda (err)
(funcall callback
(format "%s: %s"
(let ((level (flycheck-error-level err)))
(pcase level
('info (propertize "I" 'face 'flycheck-error-list-info))
('error (propertize "E" 'face 'flycheck-error-list-error))
('warning (propertize "W" 'face 'flycheck-error-list-warning))
(_ level)))
(flycheck-error-message err))
:thing (or (flycheck-error-id err)
(flycheck-error-group err))
:face 'font-lock-doc-face))
flycheck-errors)))
(defun mp-flycheck-prefer-eldoc ()
(add-hook 'eldoc-documentation-functions #'mp-flycheck-eldoc nil t)
(setq eldoc-documentation-strategy 'eldoc-documentation-compose-eagerly)
(setq flycheck-display-errors-function nil)
(setq flycheck-help-echo-function nil))
:hook ((flycheck-mode . mp-flycheck-prefer-eldoc)))
(use-package flycheck)
(use-package flycheck-eglot
:after (flycheck eglot)
@@ -530,12 +503,6 @@ Setup prefix for keybindings.
(add-hook 'before-save-hook #'eglot-format-buffer -10 t))))
(use-package eglot
:preface
(defun mp-eglot-eldoc ()
(setq eldoc-echo-area-use-multiline-p nil)
(setq eldoc-documentation-strategy
'eldoc-documentation-compose-eagerly))
:config
(add-to-list 'eglot-server-programs
'(scala-mode .
@@ -544,9 +511,6 @@ Setup prefix for keybindings.
(add-to-list 'eglot-server-programs
'(nix-mode . ("nixd")))
(add-to-list 'eglot-server-programs
'(gleam-ts-mode . ("gleam" "lsp")))
(setq-default eglot-workspace-configuration
'(
:metals (
@@ -555,11 +519,7 @@ Setup prefix for keybindings.
)
)
:custom
(eglot-code-action-indications nil)
:hook (
(eglot-managed-mode . mp-eglot-eldoc)
(go-mode . eglot-ensure)
(go-mode . alex/organize-imports-on-save)
(go-mode . alex/format-on-save)
@@ -569,9 +529,6 @@ Setup prefix for keybindings.
(nix-mode . eglot-ensure)
(nix-mode . alex/format-on-save)
(gleam-ts-mode . eglot-ensure)
(gleam-ts-mode . alex/format-on-save)
(python-mode . eglot-ensure)
(javascript-mode . eglot-ensure)
(js-mode . eglot-ensure)
@@ -588,6 +545,16 @@ Setup prefix for keybindings.
)
)
(use-package eglot-booster
:after eglot
:config (eglot-booster-mode))
#+END_SRC
** Eldoc-box
#+BEGIN_SRC emacs-lisp
(use-package eldoc-box
:after eglot
:bind (:map eglot-mode-map
("M-h" . eldoc-box-help-at-point)))
#+END_SRC
** Go
#+BEGIN_SRC emacs-lisp
@@ -606,12 +573,6 @@ Setup prefix for keybindings.
)
)
#+END_SRC
** Gleam
#+BEGIN_SRC emacs-lisp
(use-package gleam-ts-mode
:mode "\\.gleam\\'"
)
#+END_SRC
** YAML
#+BEGIN_SRC emacs-lisp
(use-package yaml-mode
+13
View File
@@ -12,6 +12,18 @@ let
epkgs.flymake-go-staticcheck
epkgs.tree-sitter-langs
epkgs.treesit-grammars.with-all-grammars
(epkgs.trivialBuild {
pname = "eglot-booster";
version = "main-2024-04-11";
src = pkgs.fetchFromGitHub {
owner = "jdtsmith";
repo = "eglot-booster";
rev = "e19dd7ea81bada84c66e8bdd121408d9c0761fe6";
hash = "sha256-vF34ZoUUj8RENyH9OeKGSPk34G6KXZhEZozQKEcRNhs=";
};
})
];
};
@@ -38,6 +50,7 @@ in
ec
emacs
pkgs.wl-clipboard
pkgs.emacs-lsp-booster
pkgs.nixd
];
};
@@ -53,12 +53,6 @@ in
package = wrapped;
# Pin the legacy profile location. The default changed to the XDG path
# (`$XDG_CONFIG_HOME/mozilla/firefox`) for stateVersion >= "26.05";
# keep the old `~/.mozilla/firefox` so the existing profile isn't
# stranded (migrating would require moving the profile dir by hand).
configPath = ".mozilla/firefox";
profiles = {
alex = {
id = 0;
+1 -1
View File
@@ -3,7 +3,7 @@
fonts.packages = [
pkgs.noto-fonts
pkgs.noto-fonts-cjk-sans
pkgs.noto-fonts-color-emoji
pkgs.noto-fonts-emoji
pkgs.nerd-fonts.jetbrains-mono
pkgs.liberation_ttf
];
+1 -6
View File
@@ -33,12 +33,7 @@ in
wayland.windowManager.hyprland = lib.mkIf hyprlandEnabled {
settings = {
bind = [
{
_args = [
"SUPER + RETURN"
(lib.generators.mkLuaInline ''hl.dsp.exec_cmd("${pkgs.foot}/bin/foot")'')
];
}
"$mod, RETURN, exec, ${pkgs.foot}/bin/foot"
];
};
};
+1 -1
View File
@@ -1,4 +1,4 @@
{ ... }:
{ pkgs, ... }:
{
home-manager.users.alex = {
programs.fzf = {
+2 -69
View File
@@ -6,38 +6,6 @@
}:
let
enabled = config.mod.git.enable;
# Wrapper around `ssh-keygen` used as git's SSH signing program. Before a
# signing operation it ensures the passphrase-protected signing key is loaded
# into the agent — `ssh-keygen -Y sign` reads the key from disk and re-prompts
# every commit otherwise, since `AddKeysToAgent` only ever caches auth keys.
# Loading it once (through the GUI askpass) lets later commits reuse the
# cached key from the agent. Verification and every other op pass straight
# through to the real ssh-keygen untouched.
sshSignWrapper = pkgs.writeShellApplication {
name = "git-ssh-sign";
runtimeInputs = [
pkgs.openssh
pkgs.gawk
pkgs.gnugrep
];
text = ''
key="${config.age.secrets."alex.pinwheel-github.com-signing".path}"
case " $* " in
*" -Y sign "*)
fp=""
fp="$(ssh-keygen -lf "$key.pub" 2>/dev/null | awk '{print $2}')" || true
if [ -n "$fp" ] && ! ssh-add -l 2>/dev/null | grep -qF "$fp"; then
# </dev/null detaches stdin so ssh-add uses SSH_ASKPASS (the GUI).
ssh-add "$key" </dev/null || true
fi
;;
esac
exec ssh-keygen "$@"
'';
};
in
{
options = {
@@ -47,7 +15,7 @@ in
};
config = lib.mkIf enabled {
home-manager.users.alex = { lib, ... }: {
home-manager.users.alex = {
programs.git = {
enable = true;
@@ -55,31 +23,11 @@ in
{ path = ./gitconfig; }
];
signing = {
key = config.age.secrets."alex.pinwheel-github.com-signing.pub".path;
signByDefault = true;
};
settings = {
extraConfig = {
rerere.enable = true;
# Tells Git to use SSH instead of the default GPG
gpg.format = "ssh";
# Sign via a wrapper that loads the signing key into the agent on
# first use, so subsequent signed commits reuse the cached key
# instead of re-prompting for the passphrase every time.
gpg.ssh.program = "${sshSignWrapper}/bin/git-ssh-sign";
};
};
home.file.".ssh/config".target = ".ssh/config_source";
home.activation.sshConfig = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
run cat ~/.ssh/config_source > ~/.ssh/config
run chmod 600 ~/.ssh/config
'';
home.packages = [ pkgs.tig ];
home.file.".tigrc".text = ''
@@ -87,20 +35,5 @@ in
set main-view-line-number-interval = 1
'';
};
age.secrets = {
"alex.pinwheel-github.com-signing" = {
file = ../../../../secrets/pinwheel/alex.pinwheel-github.com-signing.age;
path = "/home/alex/.ssh/alex.pinwheel-github.com-signing";
owner = "alex";
group = "users";
};
"alex.pinwheel-github.com-signing.pub" = {
file = ../../../../secrets/pinwheel/alex.pinwheel-github.com-signing.pub.age;
path = "/home/alex/.ssh/alex.pinwheel-github.com-signing.pub";
owner = "alex";
group = "users";
};
};
};
}
+3
View File
@@ -5,5 +5,8 @@
[url "git@github.com:"]
insteadOf = https://github.com/
[url "git@codeberg.org:"]
insteadOf = https://codeberg.org/
[url "gitea@git.ppp.pm:"]
insteadOf = https://git.ppp.pm/
-25
View File
@@ -1,25 +0,0 @@
{
pkgs,
lib,
config,
...
}:
let
enabled = config.mod.gleam.enable;
in
{
options = {
mod.gleam = {
enable = lib.mkEnableOption "enable gleam module";
};
};
config = lib.mkIf enabled {
home-manager.users.alex = {
home.packages = [
pkgs.gleam
pkgs.beamPackages.erlang
];
};
};
}
+1 -3
View File
@@ -20,9 +20,7 @@ in
enable = true;
package = pkgs.go;
env = {
GOPATH = "/home/alex/code/go";
};
goPath = "code/go";
};
home.packages = [
+1 -1
View File
@@ -22,7 +22,7 @@ in
let
session = {
user = "alex";
command = "uwsm start hyprland-uwsm.desktop";
command = "${pkgs.hyprland}/bin/Hyprland";
};
in
{
+168 -355
View File
@@ -1,5 +1,4 @@
{
inputs,
pkgs,
lib,
config,
@@ -7,73 +6,6 @@
}:
let
enabled = config.mod.hyprland.enable;
mod = "SUPER";
monitorScript = pkgs.writeShellScript "hyprland-monitor-handler" ''
INTERNAL="eDP-1"
EXTERNAL_MONITORS="HDMI-A-1 DP-3"
HYPRCTL="${pkgs.hyprland}/bin/hyprctl"
JQ="${pkgs.jq}/bin/jq"
get_active_external() {
# Return the first connected external monitor
for mon in $EXTERNAL_MONITORS; do
if $HYPRCTL monitors -j | $JQ -e ".[] | select(.name == \"$mon\")" > /dev/null 2>&1; then
echo "$mon"
return 0
fi
done
return 1
}
bind_workspaces() {
local external batch=""
if external=$(get_active_external); then
# External monitor connected: move workspaces 1-5 to external, 6-10 to internal
for ws in 1 2 3 4 5; do
batch="$batch dispatch hl.dsp.workspace.move({ monitor = \"$external\", workspace = $ws });"
done
for ws in 6 7 8 9 10; do
batch="$batch dispatch hl.dsp.workspace.move({ monitor = \"$INTERNAL\", workspace = $ws });"
done
else
# No external monitor: move all workspaces to internal
for ws in 1 2 3 4 5 6 7 8 9 10; do
batch="$batch dispatch hl.dsp.workspace.move({ monitor = \"$INTERNAL\", workspace = $ws });"
done
fi
$HYPRCTL --batch "$batch"
}
handle_event() {
case $1 in
monitoradded*|monitorremoved*)
sleep 0.5
bind_workspaces
;;
esac
}
# Start the event listener first so monitoradded events emitted during
# session startup are not lost in the gap before we begin reading them.
${pkgs.socat}/bin/socat -U - UNIX-CONNECT:"$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock" | while read -r line; do
handle_event "$line"
done &
LISTENER_PID=$!
# Give socat a moment to actually connect before the initial bind.
sleep 0.2
bind_workspaces
# Re-bind once more after the DRM subsystem has had time to enumerate
# external connectors, in case they were not yet present at session start.
(sleep 3; bind_workspaces) &
wait $LISTENER_PID
'';
in
{
options = {
@@ -83,196 +15,82 @@ in
};
config = lib.mkIf enabled {
programs.hyprland = {
enable = true;
withUWSM = true;
package = inputs.hyprland.packages.${pkgs.stdenv.hostPlatform.system}.hyprland;
portalPackage =
inputs.hyprland.packages.${pkgs.stdenv.hostPlatform.system}.xdg-desktop-portal-hyprland;
xwayland = {
enable = true;
};
};
home-manager.users.alex = {
wayland.windowManager.hyprland = {
enable = true;
systemd.enable = false;
configType = "lua";
xwayland = {
enable = true;
};
extraConfig = ''
exec-once=waybar
env = GDK_DPI_SCALE,1.5
env = XCURSOR_SIZE,64
monitor=eDP-1, 1920x1200, 0x0, 1
workspace = 1, monitor:HDMI-A-1
workspace = 2, monitor:HDMI-A-1
workspace = 3, monitor:HDMI-A-1
workspace = 4, monitor:HDMI-A-1
workspace = 5, monitor:HDMI-A-1
workspace = 6, monitor:eDP-1
workspace = 7, monitor:eDP-1
workspace = 8, monitor:eDP-1
workspace = 9, monitor:eDP-1
workspace = 10, monitor:eDP-1
workspace = w[tv1], gapsout:0, gapsin:0
workspace = f[1], gapsout:0, gapsin:0
windowrulev2 = bordersize 0, floating:0, onworkspace:w[tv1]
windowrulev2 = rounding 0, floating:0, onworkspace:w[tv1]
windowrulev2 = bordersize 0, floating:0, onworkspace:f[1]
windowrulev2 = rounding 0, floating:0, onworkspace:f[1]
exec-once=dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP
'';
settings = {
config = {
xwayland.force_zero_scaling = true;
"$mod" = "SUPER";
input = {
kb_layout = "se";
animations.enabled = false;
# 2 - Cursor focus will be detached from keyboard focus. Clicking on a window will move keyboard focus to that window.
follow_mouse = 2;
sensitivity = 0.3;
accel_profile = "flat";
touchpad = {
natural_scroll = false;
tap_and_drag = false;
};
};
cursor = {
zoom_factor = 1;
zoom_rigid = true;
};
general = {
layout = "dwindle";
gaps_in = 0; # gaps between windows
gaps_out = 0; # gaps between windows and monitor edges
col = {
active_border = "rgba(${config.lib.colors.foreground}ff)";
inactive_border = "rgba(${config.lib.colors.background}ff)";
};
};
dwindle.force_split = 2;
misc = {
disable_hyprland_logo = true;
disable_splash_rendering = true;
};
animations.enabled = false;
xwayland = {
force_zero_scaling = true;
};
monitor = [
{
output = "eDP-1";
mode = "1920x1200";
position = "auto-center-down";
scale = "1";
}
{
output = "HDMI-A-1";
mode = "2560x1440@100";
position = "auto-center-up";
scale = "1";
}
{
output = "DP-3";
mode = "2560x1440@60";
position = "auto-center-up";
scale = "1";
}
];
input = {
kb_layout = "se";
# Static defaults only: a workspace can only have one `monitor` in
# hl.workspace_rule (later calls overwrite it, unlike hyprlang's
# rule list). The hyprland-monitors service re-dispatches these to
# whichever external monitor is actually connected at startup and
# on hotplug, so this only matters for the brief window before it
# runs.
workspace_rule = [
{
workspace = "1";
monitor = "HDMI-A-1";
default = true;
}
{
workspace = "2";
monitor = "HDMI-A-1";
}
{
workspace = "3";
monitor = "HDMI-A-1";
}
{
workspace = "4";
monitor = "HDMI-A-1";
}
{
workspace = "5";
monitor = "HDMI-A-1";
}
# 2 - Cursor focus will be detached from keyboard focus. Clicking on a window will move keyboard focus to that window.
follow_mouse = 2;
{
workspace = "6";
monitor = "eDP-1";
default = true;
}
{
workspace = "7";
monitor = "eDP-1";
}
{
workspace = "8";
monitor = "eDP-1";
}
{
workspace = "9";
monitor = "eDP-1";
}
{
workspace = "10";
monitor = "eDP-1";
}
sensitivity = 0.3;
touchpad = {
natural_scroll = false;
tap-and-drag = false;
};
};
{
workspace = "w[tv1]";
gaps_out = 0;
gaps_in = 0;
}
{
workspace = "f[1]";
gaps_out = 0;
gaps_in = 0;
}
];
cursor = {
zoom_factor = 1;
zoom_rigid = true;
};
window_rule = [
{
name = "no-gaps-wtv1";
match = {
float = false;
workspace = "w[tv1]";
};
border_size = 0;
rounding = 0;
}
{
name = "no-gaps-f1";
match = {
float = false;
workspace = "f[1]";
};
border_size = 0;
rounding = 0;
}
{
# https://wiki.archlinux.org/title/Hyprland#Jetbrains_apps_focus_issues
name = "xwayland-no-initial-focus";
match.xwayland = true;
no_initial_focus = true;
}
];
general = {
layout = "dwindle";
env = [
{ _args = [ "GDK_DPI_SCALE" "1.5" ]; }
{ _args = [ "HYPRCURSOR_THEME" "Adwaita" ]; }
{ _args = [ "HYPRCURSOR_SIZE" "24" ]; }
];
gaps_in = 0; # gaps between windows
gaps_out = 0; # gaps between windows and monitor edges
on = {
_args = [
"hyprland.start"
(lib.generators.mkLuaInline ''
function()
hl.exec_cmd("uwsm app -- waybar")
hl.exec_cmd("uwsm app -- hyprctl setcursor Adwaita 24")
end
'')
];
"col.active_border" = "rgba(${config.lib.colors.foreground}ff)";
"col.inactive_border" = "rgba(${config.lib.colors.background}ff)";
};
dwindle = {
force_split = 2;
};
bind =
@@ -284,23 +102,10 @@ in
in
builtins.toString n;
focusWs = x: {
_args = [
"${mod} + ${ws x}"
(lib.generators.mkLuaInline "hl.dsp.focus({ workspace = ${builtins.toString (x + 1)} })")
];
};
moveToWs = x: {
_args = [
"${mod} + SHIFT + ${ws x}"
(lib.generators.mkLuaInline
"hl.dsp.window.move({ workspace = ${builtins.toString (x + 1)}, follow = false })"
)
];
};
select = builtins.genList focusWs 10;
move = builtins.genList moveToWs 10;
select = builtins.genList (x: "$mod, ${ws x}, workspace, ${builtins.toString (x + 1)}") 10;
move = builtins.genList (
x: "$mod SHIFT, ${ws x}, movetoworkspacesilent, ${builtins.toString (x + 1)}"
) 10;
magnifier = pkgs.writeShellScript "magnifier" ''
CURRENT=$(${pkgs.hyprland}/bin/hyprctl getoption cursor:zoom_factor -j | ${pkgs.jq}/bin/jq .float)
@@ -323,87 +128,31 @@ in
select
++ move
++ [
{
_args = [
"${mod} + ESCAPE"
(lib.generators.mkLuaInline "hl.dsp.window.close()")
];
}
"$mod, ESCAPE, killactive"
{
_args = [
"${mod} + f"
(lib.generators.mkLuaInline ''hl.dsp.window.fullscreen({ mode = "maximized" })'')
];
}
{
_args = [
"${mod} + SHIFT + f"
(lib.generators.mkLuaInline ''hl.dsp.window.float({ action = "toggle" })'')
];
}
"$mod, f, fullscreen, 1"
"$mod SHIFT, f, togglefloating, active"
{
_args = [
"${mod} + h"
(lib.generators.mkLuaInline ''hl.dsp.focus({ direction = "left" })'')
];
}
{
_args = [
"${mod} + j"
(lib.generators.mkLuaInline ''hl.dsp.focus({ direction = "down" })'')
];
}
{
_args = [
"${mod} + k"
(lib.generators.mkLuaInline ''hl.dsp.focus({ direction = "up" })'')
];
}
{
_args = [
"${mod} + l"
(lib.generators.mkLuaInline ''hl.dsp.focus({ direction = "right" })'')
];
}
"$mod, h, movefocus, l"
"$mod, j, movefocus, d"
"$mod, k, movefocus, u"
"$mod, l, movefocus, r"
{
_args = [
"${mod} + CONTROL + 1"
(lib.generators.mkLuaInline ''hl.dsp.exec_cmd("${magnifier} --increase")'')
];
}
{
_args = [
"${mod} + CONTROL + 2"
(lib.generators.mkLuaInline ''hl.dsp.exec_cmd("${magnifier} --decrease")'')
];
}
{
_args = [
"${mod} + CONTROL + 3"
(lib.generators.mkLuaInline ''hl.dsp.exec_cmd("${magnifier} --reset")'')
];
}
# mouse movements: hyprland's lua API has no separate `bindm`,
# mouse binds are plain `hl.bind` with `{ mouse = true }`.
{
_args = [
"${mod} + mouse:272" # left click
(lib.generators.mkLuaInline "hl.dsp.window.drag()")
{ mouse = true; }
];
}
{
_args = [
"${mod} + mouse:273" # right click
(lib.generators.mkLuaInline "hl.dsp.window.resize()")
{ mouse = true; }
];
}
"$mod CONTROL, 1, exec, ${magnifier} --increase"
"$mod CONTROL, 2, exec, ${magnifier} --decrease"
"$mod CONTROL, 3, exec, ${magnifier} --reset"
];
bindm = [
# mouse movements
"$mod, mouse:272, movewindow" # left click
"$mod, mouse:273, resizewindow" # right click
];
misc = {
disable_hyprland_logo = true;
disable_splash_rendering = true;
};
};
};
@@ -411,23 +160,6 @@ in
pkgs.wdisplays
pkgs.bc
];
systemd.user.services.hyprland-monitors = {
Unit = {
Description = "Hyprland monitor hotplug handler";
PartOf = [ "graphical-session.target" ];
After = [ "graphical-session.target" ];
};
Service = {
Type = "simple";
ExecStart = "${monitorScript}";
Restart = "on-failure";
RestartSec = 5;
};
Install = {
WantedBy = [ "graphical-session.target" ];
};
};
};
# To start electron apps like `chromium` with wayland support
@@ -446,6 +178,87 @@ in
# openGL is needed for wayland/hyprland
hardware.graphics.enable = true;
boot.kernelParams = [ "i915.enable_psr=0" ];
systemd.user.services.hyprland-monitors = {
# systemctl --user restart hyprland-monitors.service
# journalctl --user -u hyprland-monitors.service -e -f
unitConfig = {
Description = "handles hyprland monitor connect/disconnect";
};
wantedBy = [ "graphical-session.target" ];
requires = [ "graphical-session.target" ];
after = [ "graphical-session.target" ];
path = [
pkgs.coreutils # to include `cat`
pkgs.waybar
pkgs.hyprland
pkgs.socat
pkgs.jq
pkgs.bc
pkgs.libnotify
];
script =
let
moveWSToMonitor =
monitor: first: last:
if last < first then
throw "'first' has to be less than or equal to 'last'"
else
builtins.genList (
n: "dispatch moveworkspacetomonitor ${builtins.toString (first + n)} ${monitor}"
) (last - first + 1);
external = moveWSToMonitor "HDMI-A-1" 1 5;
internal = moveWSToMonitor "eDPI-1" 6 10;
onlyInternal = moveWSToMonitor "eDPI-1" 1 10;
in
''
update() {
HDMI_STATUS=$(cat /sys/class/drm/card1-HDMI-A-1/status)
INTERNAL_WIDTH=1920
INTERNAL_HEIGHT=1200
if [ $HDMI_STATUS = "connected" ]; then
notify-send "Using external and laptop monitor"
hyprctl keyword monitor HDMI-A-1,preferred,0x0,1
HDMI=$(hyprctl monitors -j | jq '.[] | select(.name=="HDMI-A-1")')
HDMI_WIDTH=$(echo $HDMI | jq .width)
HDMI_HEIGHT=$(echo $HDMI | jq .height)
INTERNAL_POS_X=$(echo "($HDMI_WIDTH - $INTERNAL_WIDTH) / 2" | bc)
if (( $(echo "$INTERNAL_POS_X < 0" | bc) )); then INTERNAL_POS_X=0; fi
INTERNAL_POS_Y=$HDMI_HEIGHT
hyprctl keyword monitor eDP-1,$INTERNAL_WIDTH"x"$INTERNAL_HEIGHT,$INTERNAL_POS_X"x"$INTERNAL_POS_Y,1
hyprctl --batch "${lib.strings.concatStringsSep ";" (external ++ internal)}"
else
notify-send "Using only laptop monitor"
hyprctl --batch "keyword monitor HDMI-A,disable; keyword monitor eDP-1,$INTERNAL_WIDTH"x"$INTERNAL_HEIGHT,0x0,1"
hyprctl --batch "${lib.strings.concatStringsSep ";" onlyInternal}"
fi
}
handle() {
case $1 in
monitoradded\>\>*|monitorremoved\>\>*)
echo "handling event: \"$1\""
update ;;
esac
}
echo "Starting service with instance \"$HYPRLAND_INSTANCE_SIGNATURE\""
# Do initial configuration
update
socat -U - UNIX-CONNECT:$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock | while read -r line; do handle "$line"; done
'';
};
};
}
-104
View File
@@ -1,104 +0,0 @@
{
pkgs,
lib,
config,
...
}:
let
enabled = config.mod.hyprlock.enable;
hyprlandEnabled = config.mod.hyprland.enable;
in
{
options = {
mod.hyprlock = {
enable = lib.mkEnableOption "enable hyprlock module";
dpmsTimeout = lib.mkOption {
description = "timeout in seconds before DPMS is turned on";
type = lib.types.str;
default = "10";
};
};
};
config = lib.mkIf enabled {
home-manager.users.alex = {
programs.hyprlock = {
enable = true;
settings = {
general = {
hide_cursor = true;
};
background = [
{
color = "rgb(000000)";
}
];
input-field = [
{
size = "250, 50";
position = "0, 0";
halign = "center";
valign = "center";
outline_thickness = 2;
dots_center = true;
fade_on_empty = false;
placeholder_text = "";
}
];
};
};
wayland.windowManager.hyprland = lib.mkIf hyprlandEnabled {
settings = {
bind =
let
pause-music = "${pkgs.playerctl}/bin/playerctl -a pause";
dpmsTimeout = config.mod.hyprlock.dpmsTimeout;
dpmsOff = pkgs.writeShellScript "dpms-off" ''
${pkgs.hyprland}/bin/hyprctl dispatch 'hl.dsp.dpms({ action = "off" })'
'';
dpmsOn = pkgs.writeShellScript "dpms-on" ''
${pkgs.hyprland}/bin/hyprctl dispatch 'hl.dsp.dpms({ action = "on" })'
'';
dpms-lock = pkgs.writeShellScript "dpms-lock" ''
${pkgs.swayidle}/bin/swayidle \
timeout ${dpmsTimeout} ${dpmsOff} \
resume ${dpmsOn} &
${pkgs.hyprlock}/bin/hyprlock; ${pkgs.procps}/bin/pkill swayidle
'';
in
[
{
_args = [
"SUPER + x"
(lib.generators.mkLuaInline ''hl.dsp.exec_cmd("${pause-music}; ${dpms-lock}")'')
];
}
{
_args = [
"SUPER + SHIFT + x"
(lib.generators.mkLuaInline
''hl.dsp.exec_cmd("${pause-music}; ${pkgs.hyprlock}/bin/hyprlock & sleep 0.5; systemctl suspend")''
)
];
}
];
};
};
};
security = {
polkit.enable = true;
pam.services.hyprlock = {};
};
};
}
@@ -1,6 +1,6 @@
{ pkgs, ... }:
{
home-manager.users.alex = {
home.packages = [ pkgs.typescript-language-server ];
home.packages = [ pkgs.nodePackages.typescript-language-server ];
};
}
+3 -12
View File
@@ -9,23 +9,14 @@ let
in
{
users.users.alex.extraGroups = [ "video" ];
programs.light.enable = true;
home-manager.users.alex = {
wayland.windowManager.hyprland = lib.mkIf hyprlandEnabled {
settings = {
bind = [
{
_args = [
"XF86MonBrightnessUp"
(lib.generators.mkLuaInline ''hl.dsp.exec_cmd("${pkgs.brightnessctl}/bin/brightnessctl set +5%")'')
];
}
{
_args = [
"XF86MonBrightnessDown"
(lib.generators.mkLuaInline ''hl.dsp.exec_cmd("${pkgs.brightnessctl}/bin/brightnessctl set 5%-")'')
];
}
", XF86MonBrightnessUp, exec, ${pkgs.light}/bin/light -A 5"
", XF86MonBrightnessDown, exec, ${pkgs.light}/bin/light -U 5"
];
};
};
-57
View File
@@ -1,57 +0,0 @@
{
inputs,
pkgs,
lib,
config,
...
}:
let
hyprlandEnabled = config.mod.hyprland.enable;
in
{
home-manager.users.alex = {
wayland.windowManager.hyprland = lib.mkIf hyprlandEnabled {
settings = {
bind =
let
prev = "${pkgs.playerctl}/bin/playerctl -p naviterm,spotify previous";
next = "${pkgs.playerctl}/bin/playerctl -p naviterm,spotify next";
mkExec = key: cmd: {
_args = [
key
(lib.generators.mkLuaInline ''hl.dsp.exec_cmd("${cmd}")'')
];
};
in
[
(mkExec "XF86AudioPrev" prev)
(mkExec "XF86AudioNext" next)
(mkExec "XF86AudioPlay" "${pkgs.playerctl}/bin/playerctl -p naviterm,spotify play-pause")
(mkExec "XF86AudioPause" "${pkgs.playerctl}/bin/playerctl -p naviterm,spoitfy play-pause")
(mkExec "SUPER + ALT + LEFT" prev)
(mkExec "SUPER + ALT + RIGHT" next)
(mkExec "SUPER + ALT + DOWN" "${pkgs.playerctl}/bin/playerctl -p naviterm,spotify play-pause")
];
};
};
home.packages = [
pkgs.playerctl
pkgs.spotify
inputs.naviterm.packages.${pkgs.stdenv.hostPlatform.system}.default
];
};
systemd.user.services.playerctld = {
unitConfig = {
Description = "starts playerctld daemon";
};
wantedBy = [ "default.target" ];
serviceConfig = {
ExecStart = "${pkgs.playerctl}/bin/playerctld";
};
};
}
+14 -15
View File
@@ -1,22 +1,21 @@
{ pkgs, ... }:
{
home-manager = {
users.alex = {
home.packages = [ pkgs.networkmanager ];
};
};
services.connman = {
enable = true;
networking = {
wireless.enable = false; # Wireless is managed by networkmanager
networkmanager = {
enable = true;
wifi = {
backend = "iwd";
};
wifi = {
backend = "iwd";
};
networkInterfaceBlacklist = [
"vmnet"
"vboxnet"
"virbr"
"ifb"
"ve"
"docker"
"br-"
"wg-"
];
};
networking = {
+1 -1
View File
@@ -2,7 +2,7 @@
{
home-manager.users.alex = {
home.packages = [
pkgs.nixfmt
pkgs.nixfmt-rfc-style
pkgs.nix-tree
];
};
+11 -9
View File
@@ -18,19 +18,21 @@ in
home-manager.users.alex = {
home.packages = [
pkgs.openvpn
pkgs.update-systemd-resolved
];
};
services.resolved = {
enable = true;
settings = {
Resolve = {
Domains = [ "~." ];
DNSSEC = false;
DNSOverTLS = true;
};
};
enable = false;
dnssec = "true";
domains = [ "~." ];
fallbackDns = [
"1.1.1.1#one.one.one.one"
"1.0.0.1#one.one.one.one"
];
extraConfig = ''
DNSOverTLS=yes
'';
};
};
}
+2 -14
View File
@@ -38,20 +38,8 @@ in
in
[
# will lock the screen with `physlock`, see `lockOn.suspend
{
_args = [
"SUPER + SHIFT + x"
(lib.generators.mkLuaInline ''hl.dsp.exec_cmd("${pause-music}; systemctl suspend")'')
];
}
{
_args = [
"SUPER + x"
(lib.generators.mkLuaInline
''hl.dsp.exec_cmd("${pause-music}; ${config.security.wrapperDir}/physlock -d -s -m")''
)
];
}
"$mod SHIFT, x, exec, ${pause-music}; systemctl suspend"
"$mod, x, exec, ${pause-music}; ${config.security.wrapperDir}/physlock -d -s -m"
];
};
};
+3 -3
View File
@@ -80,12 +80,12 @@ in
path = [
pkgs.coreutils # For `cat`
pkgs.libnotify
pkgs.hyprlock
pkgs.swaylock
];
script =
let
pause-music = "${pkgs.playerctl}/bin/playerctl -a pause";
pause-music = "${pkgs.playerctl}/bin/playerctl -p spotify pause";
in
''
BATTERY_CAPACITY=$(cat /sys/class/power_supply/${lowbat.battery}/capacity)
@@ -103,7 +103,7 @@ in
BATTERY_STATUS=$(cat /sys/class/power_supply/${lowbat.battery}/status)
if [[ $BATTERY_STATUS = "Discharging" ]]; then
${pause-music}; ${pkgs.hyprlock}/bin/hyprlock & sleep 0.5; systemctl suspend
${pause-music}; ${pkgs.swaylock}/bin/swaylock -f; systemctl suspend
fi
fi
'';
+3 -13
View File
@@ -8,7 +8,7 @@
let
hyprlandEnabled = config.mod.hyprland.enable;
grimblast = inputs.hyprland-contrib.packages.${pkgs.stdenv.hostPlatform.system}.grimblast;
grimblast = inputs.hyprland-contrib.packages.${pkgs.system}.grimblast;
area = "${pkgs.libnotify}/bin/notify-send 'ps: selected area' && ${grimblast}/bin/grimblast copy area";
screen = "${pkgs.libnotify}/bin/notify-send 'ps: selected screen' &&${grimblast}/bin/grimblast copy output";
in
@@ -19,18 +19,8 @@ in
wayland.windowManager.hyprland = lib.mkIf hyprlandEnabled {
settings = {
bind = [
{
_args = [
"SUPER + Print"
(lib.generators.mkLuaInline ''hl.dsp.exec_cmd("${area}")'')
];
}
{
_args = [
"SUPER + CTRL + Print"
(lib.generators.mkLuaInline ''hl.dsp.exec_cmd("${screen}")'')
];
}
"$mod, Print, exec, ${area}"
"$mod CTRL, Print, exec, ${screen}"
];
};
};
+4 -11
View File
@@ -38,19 +38,12 @@ in
MUTED=$(${pkgs.wireplumber}/bin/wpctl get-volume @DEFAULT_AUDIO_SOURCE@ | grep MUTED | wc -l)
echo $MUTED > /sys/class/leds/platform::micmute/brightness
'';
mkExec = key: cmd: {
_args = [
key
(lib.generators.mkLuaInline ''hl.dsp.exec_cmd("${cmd}")'')
];
};
in
[
(mkExec "XF86AudioRaiseVolume" "${pkgs.wireplumber}/bin/wpctl set-volume -l 1.5 @DEFAULT_AUDIO_SINK@ 2%+")
(mkExec "XF86AudioLowerVolume" "${pkgs.wireplumber}/bin/wpctl set-volume @DEFAULT_AUDIO_SINK@ 2%-")
(mkExec "XF86AudioMute" "${toggle-output-mute}")
(mkExec "XF86AudioMicMute" "${toggle-input-mute}")
", XF86AudioRaiseVolume, exec, ${pkgs.wireplumber}/bin/wpctl set-volume -l 1.5 @DEFAULT_AUDIO_SINK@ 2%+"
", XF86AudioLowerVolume, exec, ${pkgs.wireplumber}/bin/wpctl set-volume @DEFAULT_AUDIO_SINK@ 2%-"
", XF86AudioMute, exec, ${toggle-output-mute}"
", XF86AudioMicMute, exec, ${toggle-input-mute}"
];
};
};
@@ -0,0 +1,48 @@
{
pkgs,
lib,
config,
...
}:
let
hyprlandEnabled = config.mod.hyprland.enable;
in
{
home-manager.users.alex = {
wayland.windowManager.hyprland = lib.mkIf hyprlandEnabled {
settings = {
bind =
let
prev = "${pkgs.playerctl}/bin/playerctl -p spotify previous";
next = "${pkgs.playerctl}/bin/playerctl -p spotify next";
in
[
", XF86AudioPrev, exec, ${prev}"
", XF86AudioNext, exec, ${next}"
", XF86AudioPlay, exec, ${pkgs.playerctl}/bin/playerctl -p spotify play-pause"
", XF86AudioPause, exec, ${pkgs.playerctl}/bin/playerctl -p spoitfy play-pause"
"$mod ALT, LEFT, exec, ${prev}"
"$mod ALT, RIGHT, exec, ${next}"
"$mod ALT, DOWN, exec, ${pkgs.playerctl}/bin/playerctl -p spotify play-pause"
];
};
};
home.packages = [
pkgs.playerctl
pkgs.spotify
];
};
systemd.user.services.playerctld = {
unitConfig = {
Description = "starts playerctld daemon";
};
wantedBy = [ "default.target" ];
serviceConfig = {
ExecStart = "${pkgs.playerctl}/bin/playerctld";
};
};
}
+55 -66
View File
@@ -1,92 +1,55 @@
{ pkgs, ... }:
{
# Enable gnome-keyring at system level for PAM integration
services.gnome.gnome-keyring.enable = true;
# Use openssh's own ssh-agent — gcr's ssh-agent stalls signing RSA keys.
services.gnome.gcr-ssh-agent.enable = false;
programs.ssh.startAgent = true;
# Create the setuid `fusermount3` wrapper that `sshfs` execs to mount. The
# `programs.fuse` module used to install this unconditionally; newer nixpkgs
# gates it behind this option (default off), so opt in explicitly.
programs.fuse.enable = true;
home-manager.users.alex = {
services.gnome-keyring = {
enable = true;
components = [ "secrets" ];
};
home.sessionVariables = {
# gnome-keyring's PAM hooks export SSH_AUTH_SOCK pointing at a dead gcr
# socket (gcr-ssh-agent is disabled above), which shadows openssh's own
# agent and silently breaks passphrase caching. Force it back to the
# openssh agent started by `programs.ssh.startAgent`.
SSH_AUTH_SOCK = "$XDG_RUNTIME_DIR/ssh-agent";
# Route passphrase prompts through seahorse's GUI askpass instead of the
# terminal. `prefer` uses the GUI even when a tty is attached (ssh only
# falls back to askpass with no controlling terminal otherwise).
SSH_ASKPASS = "${pkgs.seahorse}/libexec/seahorse/ssh-askpass";
SSH_ASKPASS_REQUIRE = "prefer";
};
programs.ssh = {
enable = true;
enableDefaultConfig = false;
settings = {
matchBlocks = {
"manatee" = {
HostName = "manatee";
User = "alex";
IdentityFile = "/home/alex/.ssh/alex.pinwheel-manatee";
Port = 1122;
hostname = "manatee";
user = "alex";
identityFile = "/home/alex/.ssh/alex.pinwheel-manatee";
port = 1122;
};
"backwards" = {
HostName = "backwards";
User = "alex";
IdentityFile = "/home/alex/.ssh/alex.pinwheel-backwards";
Port = 1122;
hostname = "backwards";
user = "alex";
identityFile = "/home/alex/.ssh/alex.pinwheel-backwards";
port = 1122;
};
"andromeda" = {
hostname = "andromeda.a2x.se";
user = "alex";
identityFile = "/home/alex/.ssh/alex.pinwheel-andromeda";
};
"tadpole" = {
HostName = "65.21.106.222";
User = "alex";
IdentityFile = "/home/alex/.ssh/alex.pinwheel-tadpole";
Port = 1122;
hostname = "65.21.106.222";
user = "alex";
identityFile = "/home/alex/.ssh/alex.pinwheel-tadpole";
port = 1122;
};
"github.com" = {
HostName = "github.com";
IdentityFile = "/home/alex/.ssh/alex.pinwheel-github.com";
hostname = "github.com";
identityFile = "/home/alex/.ssh/alex.pinwheel-github.com";
};
"codeberg.org" = {
hostname = "codeberg.org";
identityFile = "/home/alex/.ssh/alex.pinwheel-codeberg.org";
};
"git.ppp.pm" = {
HostName = "git.ppp.pm";
IdentityFile = "/home/alex/.ssh/alex.pinwheel-git.ppp.pm";
};
"*" = {
ForwardAgent = false;
AddKeysToAgent = "yes";
Compression = false;
ServerAliveInterval = 0;
ServerAliveCountMax = 3;
HashKnownHosts = false;
UserKnownHostsFile = "~/.ssh/known_hosts";
ControlMaster = "no";
ControlPath = "~/.ssh/master-%r@%n:%p";
ControlPersist = "no";
hostname = "git.ppp.pm";
identityFile = "/home/alex/.ssh/alex.pinwheel-git.ppp.pm";
};
};
};
home.packages = [
pkgs.sshfs
pkgs.seahorse # GUI for managing gnome-keyring
];
home.packages = [ pkgs.sshfs ];
};
age.secrets = {
@@ -129,6 +92,19 @@
group = "users";
};
"alex.pinwheel-codeberg.org" = {
file = ../../../../secrets/pinwheel/alex.pinwheel-codeberg.org.age;
path = "/home/alex/.ssh/alex.pinwheel-codeberg.org";
owner = "alex";
group = "users";
};
"alex.pinwheel-codeberg.org.pub" = {
file = ../../../../secrets/pinwheel/alex.pinwheel-codeberg.org.pub.age;
path = "/home/alex/.ssh/alex.pinwheel-codeberg.org.pub";
owner = "alex";
group = "users";
};
"alex.pinwheel-git.ppp.pm" = {
file = ../../../../secrets/pinwheel/alex.pinwheel-git.ppp.pm.age;
path = "/home/alex/.ssh/alex.pinwheel-git.ppp.pm";
@@ -142,6 +118,19 @@
group = "users";
};
"alex.pinwheel-andromeda" = {
file = ../../../../secrets/pinwheel/alex.pinwheel-andromeda.age;
path = "/home/alex/.ssh/alex.pinwheel-andromeda";
owner = "alex";
group = "users";
};
"alex.pinwheel-andromeda.pub" = {
file = ../../../../secrets/pinwheel/alex.pinwheel-andromeda.pub.age;
path = "/home/alex/.ssh/alex.pinwheel-andromeda.pub";
owner = "alex";
group = "users";
};
"alex.pinwheel-tadpole" = {
file = ../../../../secrets/pinwheel/alex.pinwheel-tadpole.age;
path = "/home/alex/.ssh/alex.pinwheel-tadpole";
@@ -0,0 +1,69 @@
{
pkgs,
lib,
config,
...
}:
let
enabled = config.mod.swaylock.enable;
hyprlandEnabled = config.mod.hyprland.enable;
in
{
options = {
mod.swaylock = {
enable = lib.mkEnableOption "enable swaylock module";
dpmsTimeout = lib.mkOption {
description = "timeout in seconds before DPMS is turned on";
type = lib.types.str;
default = "10";
};
};
};
config = lib.mkIf enabled {
home-manager.users.alex = {
programs.swaylock = {
enable = true;
settings = {
color = "000000";
indicator-idle-visible = false;
show-failed-attempts = true;
};
};
wayland.windowManager.hyprland = lib.mkIf hyprlandEnabled {
settings = {
bind =
let
pause-music = "${pkgs.playerctl}/bin/playerctl -p spotify pause";
dpmsTimeout = config.mod.swaylock.dpmsTimeout;
dpms-lock = pkgs.writeShellScript "dpms-lock" ''
${pkgs.swayidle}/bin/swayidle \
timeout ${dpmsTimeout} "${pkgs.hyprland}/bin/hyprctl dispatch dpms off" \
resume "${pkgs.hyprland}/bin/hyprctl dispatch dpms on" &
${pkgs.swaylock}/bin/swaylock && ${pkgs.procps}/bin/pkill swayidle
'';
in
[
"$mod, x, exec, ${pause-music}; ${dpms-lock}"
"$mod SHIFT, x, exec, ${pause-music}; ${pkgs.swaylock}/bin/swaylock -f; systemctl suspend"
];
};
};
};
security = {
polkit.enable = true;
pam.services.swaylock.text = ''
# PAM configuration file for the swaylock screen locker. By default, it includes
# the 'login' configuration file (see /etc/pam.d/login)
auth include login
'';
};
};
}
+26 -2
View File
@@ -15,6 +15,7 @@
settings = {
devices = {
phone.id = config.lib.syncthing.phone;
backwards.id = config.lib.syncthing.backwards;
manatee.id = config.lib.syncthing.manatee;
};
@@ -23,6 +24,7 @@
path = "/home/alex/sync/org";
devices = [
"phone"
"backwards"
"manatee"
];
versioning = {
@@ -35,7 +37,7 @@
personal = {
path = "/home/alex/sync/personal";
devices = [ "manatee" ];
devices = [ "backwards" ];
versioning = {
type = "staggered";
params = {
@@ -46,7 +48,29 @@
work = {
path = "/home/alex/sync/work";
devices = [ "manatee" ];
devices = [ "backwards" ];
versioning = {
type = "staggered";
params = {
maxAge = "2592000"; # 30 days
};
};
};
books = {
path = "/home/alex/sync/reading-material/books";
devices = [ "backwards" ];
versioning = {
type = "staggered";
params = {
maxAge = "2592000"; # 30 days
};
};
};
ttrpg = {
path = "/home/alex/sync/reading-material/ttrpg";
devices = [ "backwards" ];
versioning = {
type = "staggered";
params = {
+1 -17
View File
@@ -1,23 +1,7 @@
{ pkgs, ... }:
{ ... }:
{
services.tailscale.enable = true;
# Pinned to 1.96.5. 1.98.0 regressed split-DNS handling under work-vpn: the
# netmap's "resolve <tailnet>.ts.net locally via MagicDNS" hint is dropped
# when translated into systemd-resolved config, so *.ts.net queries get sent
# to a public resolver (199.247.155.53) that the corporate VPN's port-53
# egress filter blocks.
services.tailscale.package = pkgs.tailscale.overrideAttrs (_: rec {
version = "1.96.5";
src = pkgs.fetchFromGitHub {
owner = "tailscale";
repo = "tailscale";
tag = "v${version}";
hash = "sha256-vYYb+2OtuXftjGGG0zWJesHccrClB8YZpclv9KzNN/c=";
};
vendorHash = "sha256-rhuWEEN+CtumVxOw6Dy/IRxWIrZ2x6RJb6ULYwXCQc4=";
});
networking.firewall = {
checkReversePath = "loose";
allowedUDPPorts = [ 41641 ];
+1 -1
View File
@@ -18,7 +18,7 @@
# https://old.reddit.com/r/tmux/comments/mesrci/tmux_2_doesnt_seem_to_use_256_colors/
set -g default-terminal "xterm-256color"
set -ga terminal-overrides ",*256col*:Tc"
set -ga terminal-overrides ',*:Ss=\E[%p1%d q:Se=\E[ q'
set -ga terminal-overrides "*:Ss=\E[%p1%d q:Se=\E[ q"
set-environment -g COLORTERM "truecolor"
set-option -g allow-rename off
+1 -1
View File
@@ -18,7 +18,7 @@ in
};
programs.git = lib.mkIf gitEnabled {
settings = {
extraConfig = {
core = {
editor = "vim";
};
+11 -29
View File
@@ -7,14 +7,14 @@
let
hyprlandEnabled = config.mod.hyprland.enable;
music-status = pkgs.writeShellScript "music-status" ''
STATUS=$(${pkgs.playerctl}/bin/playerctl -p naviterm,spotify status 2>&1)
spotify-status = pkgs.writeShellScript "spotify-status" ''
STATUS=$(${pkgs.playerctl}/bin/playerctl -p spotify status 2>&1)
if [ "$STATUS" = "No players found" ]; then
echo ""
else
FORMAT="{{markup_escape(xesam:title)}} - {{markup_escape(xesam:artist)}}"
OUTPUT=$(${pkgs.playerctl}/bin/playerctl -p naviterm,spotify metadata --format "$FORMAT")
OUTPUT=$(${pkgs.playerctl}/bin/playerctl -p spotify metadata --format "$FORMAT")
case "$STATUS" in
"Playing")
echo "<span font='14' rise='-3000'></span> $OUTPUT"
@@ -85,25 +85,11 @@ let
'';
work-vpn-status = pkgs.writeShellScript "work-vpn-status" ''
ACTIVE_ENVS=""
STAGING=$(systemctl is-active openvpn-work-staging.service)
[ "$STAGING" == "active" ] && echo "WORK-VPN STAGING ON" && exit 0
STAGING_STATUS=$(systemctl is-active openvpn-work-staging.service)
if [ "$STAGING_STATUS" == "active" ]; then
ACTIVE_ENVS="S"
fi
PRODUCTION_STATUS=$(systemctl is-active openvpn-work-production.service)
if [ "$PRODUCTION_STATUS" == "active" ]; then
if [ -n "$ACTIVE_ENVS" ]; then
ACTIVE_ENVS="$ACTIVE_ENVS&amp;P"
else
ACTIVE_ENVS="P"
fi
fi
if [ -n "$ACTIVE_ENVS" ]; then
echo "WORK-VPN $ACTIVE_ENVS ON"
fi
PRODUCTION=$(systemctl is-active openvpn-work-production.service)
[ "$PRODUCTION" == "active" ] && echo "WORK-VPN PRODUCTION ON" && exit 0
'';
toggle-bt-power = pkgs.writeShellScript "toggle-bt-power" ''
@@ -140,7 +126,7 @@ in
modules-left = lib.mkIf hyprlandEnabled [ "hyprland/workspaces" ];
modules-right = [
"custom/work-vpn-status"
"custom/music"
"custom/spotify"
"custom/container-status"
"custom/dunst"
"bluetooth"
@@ -156,8 +142,8 @@ in
interval = 2;
};
"custom/music" = {
exec = music-status;
"custom/spotify" = {
exec = spotify-status;
interval = 2;
max-length = 70;
tooltip = false;
@@ -208,7 +194,6 @@ in
};
battery = {
"bat" = "BAT0";
"interval" = 60;
"format" = "<span font='10' rise='1000'>{icon}</span> {capacity}%";
"format-time" = "{H}h {M}min";
@@ -237,10 +222,7 @@ in
height = 30;
spacing = 20;
fixed-center = false;
output = [
"HDMI-A-1"
"DP-3"
];
output = [ "HDMI-A-1" ];
modules-left = lib.mkIf hyprlandEnabled [ "hyprland/workspaces" ];
modules-right = [
+1 -6
View File
@@ -78,12 +78,7 @@ in
wayland.windowManager.hyprland = lib.mkIf hyprlandEnabled {
settings = {
bind = [
{
_args = [
"SUPER + RETURN"
(lib.generators.mkLuaInline ''hl.dsp.exec_cmd("${pkgs.wezterm}/bin/wezterm")'')
];
}
"$mod, RETURN, exec, ${pkgs.wezterm}/bin/wezterm"
];
};
};
+29 -88
View File
@@ -1,5 +1,4 @@
{
inputs,
pkgs,
lib,
config,
@@ -12,98 +11,47 @@ let
in
{
home-manager.users.alex = {
# Ensure bashInteractive is first in PATH inside nix devshells.
# stdenv provides a non-interactive bash that breaks Copilot shell commands.
# Adding bashInteractive to home.packages alone isn't enough because devshell
# packages are prepended to PATH. This precmd hook runs after direnv's hook
# and re-prepends bashInteractive so it takes priority.
programs.zsh.initContent = ''
_ensure_bash_interactive() {
[[ "$PATH" == "${pkgs.bashInteractive}/bin:"* ]] || export PATH="${pkgs.bashInteractive}/bin:$PATH"
}
precmd_functions+=(_ensure_bash_interactive)
# Source the zsh-specific rc file that nix-direnv emits ($DIRENV_ZSH_RC)
# so devshell completions and zsh setup are picked up. direnv itself only
# exports env vars, so without this hook the zsh side of the devshell is
# never loaded. Guarded by LAST_LOADED_DIRENV_ZSH_RC so we don't re-source
# it on every precmd.
_nix_direnv_bridge_hook() {
if [[ -n "$DIRENV_ZSH_RC" && "$LAST_LOADED_DIRENV_ZSH_RC" != "$DIRENV_ZSH_RC" ]]; then
if [[ -f "$DIRENV_ZSH_RC" ]]; then
source "$DIRENV_ZSH_RC"
export LAST_LOADED_DIRENV_ZSH_RC="$DIRENV_ZSH_RC"
echo " direnv zsh loaded..."
fi
fi
}
autoload -Uz add-zsh-hook
add-zsh-hook precmd _nix_direnv_bridge_hook
'';
# Configure IntelliJ to exclude .direnv from indexing
home.activation.intellijIgnoreDirenv = ''
for idea_config in $HOME/.config/JetBrains/IntelliJIdea*; do
if [ -d "$idea_config" ]; then
$DRY_RUN_CMD mkdir -p "$idea_config/options"
$DRY_RUN_CMD tee "$idea_config/options/filetypes.xml" > /dev/null <<'EOF'
<application>
<component name="FileTypeManager" version="18">
<ignoreFiles list="*.pyc;*.pyo;*.rbc;*.yarb;*~;.DS_Store;.git;.hg;.svn;CVS;__pycache__;_svn;vssver.scc;vssver2.scc;.direnv" />
</component>
</application>
EOF
fi
done
'';
home.sessionVariables = {
GITHUB_ACTOR = "Alexander Heldt";
GITHUB_TOKEN = "$(${pkgs.coreutils}/bin/cat ${config.age.secrets.work-github-token.path})";
};
home.packages = [
# (pkgs.callPackage ./pants.nix { inherit (pkgs) stdenv.hostPlatform.system; })
# (pkgs.callPackage ./syb-cli.nix { })
home.packages =
let
intellij = (
pkgs.jetbrains.idea-ultimate.overrideAttrs (
final: prev: {
version = "2024.2.4";
src = pkgs.fetchurl {
url = "https://download.jetbrains.com/idea/ideaIU-${final.version}.tar.gz";
sha256 = "8411fda793a20356a4982e4f18f6691839d8a471e2081ab6d8cc78b3f8b02532";
};
}
)
);
in
[
# (pkgs.callPackage ./pants.nix { inherit (pkgs) system; })
# (pkgs.callPackage ./syb-cli.nix { })
(inputs.nix-jetbrains-plugins.lib.buildIdeWithPlugins pkgs "idea" [
"IdeaVIM"
"com.github.copilot"
])
pkgs.bashInteractive
(pkgs.jetbrains.plugins.addPlugins intellij [ "ideavim" ])
(pkgs.google-cloud-sdk.withExtraComponents [
pkgs.google-cloud-sdk.components.gke-gcloud-auth-plugin
])
(pkgs.google-cloud-sdk.withExtraComponents [
pkgs.google-cloud-sdk.components.gke-gcloud-auth-plugin
])
pkgs.graphite-cli
pkgs.graphite-cli
pkgs.postman
pkgs.grpcurl
pkgs.postman
pkgs.grpcurl
pkgs.slack
# for `radio`
pkgs.go-mockery
pkgs.golangci-lint
(pkgs.writeShellScriptBin "work-vpn" ''
case $1 in
up)
sudo sh -c "systemctl start openvpn-work-staging.service; systemctl start openvpn-work-production.service"
;;
down)
sudo sh -c "systemctl stop openvpn-work-staging.service; systemctl stop openvpn-work-production.service"
;;
esac
'')
];
# for `radio`
pkgs.go-mockery
pkgs.golangci-lint
];
programs.go = lib.mkIf goEnabled {
env = {
GOPRIVATE = [ "$(${pkgs.coreutils}/bin/cat ${config.age.secrets.work-go-private.path})" ];
};
goPrivate = [ "$(${pkgs.coreutils}/bin/cat ${config.age.secrets.work-go-private.path})" ];
};
programs.git = lib.mkIf gitEnabled {
@@ -116,13 +64,6 @@ EOF
};
};
# Needed for `copilot`
programs.nix-ld.enable = true;
programs.nix-ld.libraries = [
pkgs.stdenv.cc.cc.lib
pkgs.zlib
];
services.openvpn.servers = lib.mkIf openvpnEnabled {
work-staging = {
config = "config ${config.age.secrets.work-staging-ovpn.path}";
+2 -1
View File
@@ -1,4 +1,5 @@
{
system,
pkgs,
lib,
...
@@ -10,7 +11,7 @@ let
match =
v: l: builtins.elemAt (lib.lists.findFirst (x: (if_let v (builtins.elemAt x 0)) != null) null l) 1;
package = match { platform = pkgs.stdenv.hostPlatform.system; } [
package = match { platform = system; } [
[
{ platform = "aarch64-linux"; }
{
-8
View File
@@ -56,14 +56,6 @@ in
initContent = lib.strings.concatStringsSep "\n" [
"export KEYTIMEOUT=1"
# Point every interactive shell at openssh's ssh-agent. home-manager's
# session vars set this too, but hm-session-vars runs once and is then
# inherited — so a tmux server that outlives this change (or started
# with the stale gcr socket) hands new panes a dead SSH_AUTH_SOCK.
# Re-exporting the fixed path per-shell keeps every pane on the same
# agent, so each key is only ever prompted for once per session.
''export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent"''
"bindkey -v '^?' backward-delete-char"
"bindkey '^a' beginning-of-line"
"bindkey '^e' end-of-line"
+1 -1
View File
@@ -8,7 +8,7 @@
};
environment.systemPackages = [
inputs.agenix.packages."${pkgs.stdenv.hostPlatform.system}".default
inputs.agenix.packages."${pkgs.system}".default
];
};
}
-5
View File
@@ -18,11 +18,6 @@
group = "nginx";
};
"whib.ppp.pm" = {
webroot = "/var/lib/acme/acme-challenge/";
group = "nginx";
};
"api.whib.ppp.pm" = {
webroot = "/var/lib/acme/acme-challenge/";
group = "nginx";
-1
View File
@@ -23,7 +23,6 @@ in
pppdotpm-site.enable = true;
whib-backend.enable = true;
whib-frontend.enable = true;
};
};
}
-4
View File
@@ -67,10 +67,6 @@ in
HTTP_PORT = 3001;
};
oauth2 = {
JWT_CLAIM_ISSUER = "https://${gitDomain}/";
};
database = {
type = "sqlite3";
passwordFile = config.age.secrets.gitea-dbpassword.path;
+20 -17
View File
@@ -21,25 +21,16 @@ in
home-manager.users.alex = {
programs.ssh = {
enable = true;
enableDefaultConfig = false;
settings = {
matchBlocks = {
"git.ppp.pm" = {
HostName = "git.ppp.pm";
IdentityFile = "/home/alex/.ssh/alex.tadpole-git.ppp.pm";
hostname = "git.ppp.pm";
identityFile = "/home/alex/.ssh/alex.tadpole-git.ppp.pm";
};
"*" = {
ForwardAgent = false;
AddKeysToAgent = "no";
Compression = false;
ServerAliveInterval = 0;
ServerAliveCountMax = 3;
HashKnownHosts = false;
UserKnownHostsFile = "~/.ssh/known_hosts";
ControlMaster = "no";
ControlPath = "~/.ssh/master-%r@%n:%p";
ControlPersist = "no";
"codeberg.org" = {
hostname = "codeberg.org";
identityFile = "/home/alex/.ssh/alex.tadpole-codeberg.org";
};
};
};
@@ -49,7 +40,6 @@ in
mode = "0755";
text = ''
#!${pkgs.bash}/bin/bash
[ "$1" = "alex" ] || exit 0
for file in ${authorizedKeysPath}/*; do
${pkgs.coreutils}/bin/cat "$file"
done
@@ -73,7 +63,7 @@ in
KbdInteractiveAuthentication = false;
};
authorizedKeysCommand = "/etc/ssh/authorized_keys_command %u";
authorizedKeysCommand = "/etc/ssh/authorized_keys_command";
authorizedKeysCommandUser = "root";
};
};
@@ -111,6 +101,19 @@ in
owner = "alex";
group = "users";
};
"alex.tadpole-codeberg.org" = {
file = ../../../../secrets/tadpole/alex.tadpole-codeberg.org.age;
path = "/home/alex/.ssh/alex.tadpole-codeberg.org";
owner = "alex";
group = "users";
};
"alex.tadpole-codeberg.org.pub" = {
file = ../../../../secrets/tadpole/alex.tadpole-codeberg.org.pub.age;
path = "/home/alex/.ssh/alex.tadpole-codeberg.org.pub";
owner = "alex";
group = "users";
};
};
};
}
+20 -35
View File
@@ -4,64 +4,49 @@
...
}:
let
backendEnabled = config.mod.whib-backend.enable;
frontendEnabled = config.mod.whib-frontend.enable;
enabled = config.mod.whib-backend.enable;
in
{
options = {
mod.whib-backend = {
enable = lib.mkEnableOption "enable WHIB backend";
};
mod.whib-frontend = {
enable = lib.mkEnableOption "enable WHIB frontend";
};
};
config = {
config = lib.mkIf enabled {
assertions = [
{
assertion = backendEnabled && config.services.nginx.enable;
assertion = config.services.nginx.enable;
message = "Option 'config.services.nginx' must be enabled";
}
];
services = {
whib-backend = lib.mkIf backendEnabled {
enable = true;
services.whib-backend = {
enable = true;
backend = {
domain = "api.whib.ppp.pm";
useACMEHost = "api.whib.ppp.pm";
backend = {
domain = "api.whib.ppp.pm";
useACMEHost = "api.whib.ppp.pm";
environmentFile = config.age.secrets.whib-backend-env-vars.path;
};
environmentFile = config.age.secrets.whib-backend-env-vars.path;
};
postgres = {
environmentFile = config.age.secrets.whib-postgres-env-vars.path;
postgres = {
environmentFile = config.age.secrets.whib-postgres-env-vars.path;
backup = {
interval = "*-*-* 00:00:00 UTC";
backup = {
interval = "*-*-* 00:00:00 UTC";
environmentFile = config.age.secrets.whib-postgres-backup-env-vars.path;
gpgPassphraseFile = config.age.secrets.whib-gpg-key.path;
};
};
grafana = {
domain = "grafana.whib.ppp.pm";
useACMEHost = "grafana.whib.ppp.pm";
environmentFile = config.age.secrets.whib-grafana-env-vars.path;
environmentFile = config.age.secrets.whib-postgres-backup-env-vars.path;
gpgPassphraseFile = config.age.secrets.whib-gpg-key.path;
};
};
whib-frontend = lib.mkIf frontendEnabled {
enable = true;
grafana = {
domain = "grafana.whib.ppp.pm";
useACMEHost = "grafana.whib.ppp.pm";
domain = "whib.ppp.pm";
useACMEHost = "whib.ppp.pm";
backendHost = "https://api.whib.ppp.pm";
environmentFile = config.age.secrets.whib-grafana-env-vars.path;
};
};
+2 -4
View File
@@ -3,7 +3,6 @@
imports = [
./ppp.pm-site.nix
./whib-backend.nix
./whib-frontend.nix
];
config = {
@@ -14,7 +13,6 @@
mod = {
pppdotpm-site.enable = false;
whib-backend.enable = true;
whib-frontend.enable = true;
};
users.users.a = {
@@ -26,9 +24,9 @@
security.sudo.wheelNeedsPassword = false;
virtualisation.vmVariant = {
# following configuration is added only when building VM the *first* time with `build-vm`
# following configuration is added only when building VM with build-vm
virtualisation = {
diskSize = 8192;
diskSize = 4096;
memorySize = 2048;
cores = 3;
graphics = false;
-35
View File
@@ -1,35 +0,0 @@
{ lib, config, ... }:
let
enabled = config.mod.whib-frontend.enable;
in
{
options = {
mod.whib-frontend = {
enable = lib.mkEnableOption "enable WHIB backend";
};
};
config = lib.mkIf enabled {
services.whib-frontend = {
enable = true;
domain = "whib-frontend.local";
port = "8081";
# backendHost = "https://api.whib.ppp.pm/";
backendHost = "http://localhost:8080";
};
virtualisation.vmVariant = {
virtualisation = {
forwardPorts = [
{
# Service API
from = "host";
host.port = 8081;
guest.port = 8081;
}
];
};
};
};
}
Binary file not shown.
@@ -0,0 +1,7 @@
age-encryption.org/v1
-> ssh-ed25519 Pu0HWg eK/pdhmsF334C7rSuYsRnXCtenmlT2hOXpfW5CQEARY
odooTLu8ZQUZjCeVPZYOA6Vgb470cosE1Q1iBkE9Kc0
-> ssh-ed25519 +oNaHQ nJU52SSZ9v3+8NuXR6coSHosEYrs7T8GeZYzV/quOU4
IV5YduRGdJLy93gVwfYmwvldRXoXXX3QvAsH3ljBadw
--- 3gJg9NFmqHCrgcvgnYOeSY1H4klPEyzI+07IlKCOItc
 ¦Ì\5çܤ‰}õyñÐáAý_J§ õ“{ÂÉp± +ꜞ+¹JØeìñLìW¯å¯éµ 7šÉ=S*³¥É}fPܽқÒÉ!X©ê7úÎ3¡øœ³\`S•è‰iºözW¸À©<4Pb™ˆ—€ZI’©hH˜îÃî¦61õ'™é…ybmCmE
@@ -0,0 +1,7 @@
age-encryption.org/v1
-> ssh-ed25519 Pu0HWg rTAkGJbth0WCE8KM50fHaCyXeO/NrmWXiDT/JH9ciAI
kTMVbJRwOSh9Da1O9YYx21/7IdfZrb2OhoOJxNEIKSg
-> ssh-ed25519 +oNaHQ DtMpPTuAjS1GyK2WalNJzErEE1mCos/R5aZyMnun+UQ
B81FnJ5z70HzI6yvqiy6Lhr2X9ZjEi5dhM6u47flujA
--- r9HCFWVU5LBiRBdRwOA1+IRBY1/I/1UeukGtFz7BxWE
TÔK%£žWÔ*&˜İ-&àÛÕ8)|×áॣù Œ¯è07µ¾ªŒúÍòl»ÁQò0†ÒßÔ––ÆÂ]¬¬Û¸”mP”^~ç/þ#†šõæ"ëÀš+ž)ÕY¾n¼@ã჌R§“¨_pÓÂFÜÛ%᤼#
@@ -0,0 +1,7 @@
age-encryption.org/v1
-> ssh-ed25519 Pu0HWg tZ6zAXOBdiWyyUeOZZ66w1ij8xuHY98fvClPn8/jQVs
AVp3Y04vSbnkurqjAouiDojd5IMFVCYyldXv0v4n9W4
-> ssh-ed25519 +oNaHQ KK44MdrfQLZK44eYWpLiTFm3d/bx6WTsHm98MkvhQTw
CYJJWbpHbLQHvzTWLbujg1AZ3KvgCshVUrolPE1hUho
--- aBOxH3rbMriVBctdVGdQXFH/KYWLbweGzda5sN4HJOA
íNo´Ç¡O>š‡æãõ\IÉ#0¬‰õ+Y8ƒvàCS#éO›žÖeGè;-T“d®,V@3” 䛸þ]`Ç\b²Dõ+„ö6½öšð䘤nŠ%‡NÈ’
+7
View File
@@ -0,0 +1,7 @@
age-encryption.org/v1
-> ssh-ed25519 Pu0HWg qmcWFPndrhXlpjBtSsVNARAOHM9UNtfcEvtSGx/BLHY
EdnzUWju9g61idxWmDaaxSZ6ZiVvhFLOKH3hY1Kyk8s
-> ssh-ed25519 +oNaHQ CMBvVWHwVsY89rbdYvVoVeoZlIXLJoIN8xsoqqBnSiA
+xogVU/MBezQzq1rbIOqT5PGNYqM8o0Pmojk2npqT4U
--- mdhPf3weI9cOcaAh9j/CVB+KRfPSRuT678oueeJjdCk
ZñmݹÿÅÓº*ˆŒ9kÀw¶N¯Zh&ÈGô ßkÞÀºÆŸþŸA#ª“lù u¤Qôïù—›ÛîTÔŸ×˜Ž²Ž,ßCV3†Ä µ”
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More