organize, update nixcord, add janitor

This commit is contained in:
4DBug
2026-03-03 09:15:54 -06:00
parent a117ed02a2
commit c8361a4c57
9 changed files with 382 additions and 58 deletions

View File

@@ -2,4 +2,4 @@
# Do not delete.
folderID: nix
created: 2026-01-29T23:05:44-06:00
created: 2026-01-29T23:06:32-06:00

View File

@@ -1,31 +1,67 @@
# nix
[dendritic](https://github.com/vic/den) [nix](https://nixos.org/) configuration.
my [nixos](https://nixos.org/) configuration, structured with [den](https://github.com/vic/den) and [flake-parts](https://github.com/hercules-ci/flake-parts).
## structure
```nix/flake.nix#L1-2
outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } (inputs.import-tree ./modules);
```
modules are automatically discovered via [import-tree](https://github.com/vic/import-tree) and composed using `den.aspects`.
```
modules/
├── core/ # System fundamentals (desktop, audio, graphics, network, theme)
├── hosts/ # Per-machine includes (nix, laptop, box)
├── infra/ # Infrastructure (syncthing, cloudflared tunnels)
├── services/ # Self-hosted services (searxng, gitea, copyparty, ...)
├── software/ # User software (firefox, nixcord, beets, ...)
└── users/ # User accounts
```
---
## hosts
| host | command |
|-|-|
| [desktop](https://github.com/4DBug/nix/tree/master/modules/hosts/nix/nix.nix) | `nh os switch ~/nix --impure -H nix` |
| [laptop](https://github.com/4DBug/nix/tree/master/modules/hosts/nix/laptop.nix) | `nh os switch ~/nix --impure -H laptop` |
| [server](https://github.com/4DBug/nix/tree/master/modules/hosts/nix/box.nix) | `nh os switch ~/nix --impure -H box` |
## services
| service | location | file |
|-|-|-|
| [invidious](https://github.com/invidious/invidious) | [tube.bug.tools](https://tube.bug.tools/) | /modules/services |
| [glances](https://github.com/nicolargo/glances) | [monitor.bug.tools](https://monitor.bug.tools/) | /modules/services |
| [searxng](https://github.com/searxng/searxng) | [search.bug.tools](https://search.bug.tools/) | /modules/services |
| [redlib](https://github.com/redlib-org/redlibb) | [reddit.bug.tools](https://reddit.bug.tools/) | ./modules/services/searxng/searxng.nix |
| [copyparty](https://github.com/9001/copyparty) | [files.bug.tools](https://files.bug.tools/) | /modules/services |
| [matrix](https://matrix.org/) | [matrix.bug.tools](https://matrix.bug.tools/) | /modules/services |
| [nixos-mailserver](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver) | [mail.bug.tools](https://mail.bug.tools/) | /modules/services |
| [sish](https://docs.ssi.sh/) | [tuns.bug.tools](https://tuns.bug.tools/) | /modules/services |
| [vscode-server](https://github.com/cdr/code-server) | - | /modules/services |
| Host | Role | GPU | Switch Command |
|------|------|-----|----------------|
| `nix` | desktop | nvidia | `nh os switch ~/nix --impure -H nix` |
| `laptop` | laptop | amd | `nh os switch ~/nix --impure -H laptop` |
| `box` | homeserver | — | `nh os switch ~/nix --impure -H box` |
## software
---
| software | file |
|-|-|
| a | b |
| c | d |
| e | f |
## self-hosted services
all services on `box` are proxied through cloudflared tunnels to `*.bug.tools`.
| Service | URL | Description |
|---------|-----|-------------|
| [searxng](https://github.com/searxng/searxng) | [search.bug.tools](https://search.bug.tools) | Privacy-respecting metasearch engine |
| [redlib](https://github.com/redlib-org/redlib) | [reddit.bug.tools](https://reddit.bug.tools) | Private Reddit frontend *(disabled)* |
| [copyparty](https://github.com/9001/copyparty) | [files.bug.tools](https://files.bug.tools) | File server / uploader |
| [gitea](https://gitea.io/) | [git.bug.tools](https://git.bug.tools) | Self-hosted Git with org mirrors |
| [glances](https://github.com/nicolargo/glances) | [monitor.bug.tools](https://monitor.bug.tools) | System monitoring |
| [nixos-mailserver](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver) | [mail.bug.tools](https://mail.bug.tools) | Email server |
| [invidious](https://github.com/iv-org/invidious) | [tube.bug.tools](https://tube.bug.tools) | YouTube frontend *(disabled)* |
| [matrix](https://matrix.org/) | [matrix.bug.tools](https://matrix.bug.tools) | Matrix homeserver *(disabled)* |
| [sish](https://docs.ssi.sh/) | [tuns.bug.tools](https://tuns.bug.tools) | SSH tunnels *(disabled)* |
| vscode-server | — | Remote VS Code access |
---
## flake inputs
| Input | Purpose |
|-------|---------|
| [nixpkgs](https://github.com/nixos/nixpkgs) (unstable) | Package set |
| [home-manager](https://github.com/nix-community/home-manager) | User environment management |
| [flake-parts](https://github.com/hercules-ci/flake-parts) | Flake structure |
| [den](https://github.com/vic/den) | Dendritic aspect system |
| [import-tree](https://github.com/vic/import-tree) | Auto module discovery |
| [stylix](https://github.com/nix-community/stylix) | System-wide theming |
| [catppuccin/nix](https://github.com/catppuccin/nix) | Catppuccin theme modules |
| [nixcord](https://github.com/FlameFlag/nixcord) | Declarative Vesktop/Discord |
| [nix-flatpak](https://github.com/gmodena/nix-flatpak) | Declarative Flatpak |
| [copyparty](https://github.com/9001/copyparty) | File server |

View File

@@ -1,4 +1,4 @@
{ den, ... }: {
{
den.aspects.gitea-mirrors = {
nixos = { pkgs, lib, config, ... }:
let
@@ -13,6 +13,12 @@
giteaUrl = "http://localhost:${toString cfg.settings.server.HTTP_PORT}";
tokenFile = "${cfg.stateDir}/mirror-setup-token";
repoSource = m:
if m ? source then m.source
else if (m.service or "git") == "github" then "https://github.com/${m.owner}/${m.repo}.git"
else if (m.service or "git") == "gitlab" then "https://gitlab.com/${m.owner}/${m.repo}.git"
else throw "mirrors.repos: '${m.owner}/${m.repo}' has no source and service '${m.service or "git"}' needs an explicit source";
setupScript = pkgs.writeShellScript "gitea-mirror-setup" ''
set -euo pipefail
@@ -20,7 +26,6 @@
TOKEN_FILE="${tokenFile}"
GITEA_CMD="${lib.getExe cfg.package}"
# wait for gitea to be ready
for i in $(seq 1 60); do
if ${pkgs.curl}/bin/curl -sf "$GITEA_URL/api/v1/version" >/dev/null 2>&1; then
break
@@ -29,15 +34,15 @@
done
$GITEA_CMD admin user create \
--username "${mcfg.adminUser}" \
--email "${mcfg.adminEmail}" \
--username "${mcfg.admin.user}" \
--email "${mcfg.admin.email}" \
--random-password \
--admin \
--must-change-password=false 2>/dev/null || true
if [ ! -f "$TOKEN_FILE" ]; then
TOKEN=$($GITEA_CMD admin user generate-access-token \
--username "${mcfg.adminUser}" \
--username "${mcfg.admin.user}" \
--token-name "mirror-setup" \
--scopes "all" \
--raw)
@@ -55,7 +60,7 @@
-H "Content-Type: application/json" \
-d "{
\"username\": \"${owner}\",
\"email\": \"${owner}@mirror.localhost\",
\"email\": \"${owner}@git.bug.tools\",
\"password\": \"$RAND_PASS\",
\"must_change_password\": false,
\"visibility\": \"public\"
@@ -67,7 +72,7 @@
-H "$AUTH" \
-H "Content-Type: application/json" \
-d '{
"clone_addr": "${m.source}",
"clone_addr": "${repoSource m}",
"repo_name": "${m.repo}",
"repo_owner": "${m.owner}",
"mirror": true,
@@ -134,24 +139,27 @@
in
{
options.services.gitea.mirrors = {
adminUser = lib.mkOption {
type = lib.types.str;
default = "admin";
description = "Gitea admin username for mirror management.";
};
admin = {
user = lib.mkOption {
type = lib.types.str;
default = "admin";
description = "Gitea admin username for mirror management.";
};
adminEmail = lib.mkOption {
type = lib.types.str;
default = "admin@localhost";
description = "Gitea admin email.";
email = lib.mkOption {
type = lib.types.str;
default = "admin@localhost";
description = "Gitea admin email.";
};
};
repos = lib.mkOption {
type = lib.types.listOf (lib.types.attrsOf lib.types.str);
default = [];
description = "Individual repositories to mirror. Each entry: { owner, repo, source, service? }.";
description = "Individual repositories to mirror. Each entry: { owner, repo, service?, source? }. Source is auto-derived for github/gitlab.";
example = [
{ owner = "nixos"; repo = "nixpkgs"; source = "https://github.com/NixOS/nixpkgs.git"; service = "github"; }
{ owner = "nixos"; repo = "nixpkgs"; service = "github"; }
{ owner = "someone"; repo = "thing"; source = "https://custom.instance/someone/thing.git"; }
];
};
@@ -180,11 +188,9 @@
description = "Setup Gitea mirror repositories";
after = [ "gitea.service" ];
requires = [ "gitea.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = cfg.user;
Group = cfg.group;
ExecStart = setupScript;
@@ -195,6 +201,17 @@
];
};
};
systemd.timers.gitea-mirror-setup = {
description = "Run Gitea mirror setup on boot and periodically";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "2min";
OnUnitActiveSec = "6h";
Unit = "gitea-mirror-setup.service";
};
};
};
};
};

View File

@@ -15,16 +15,19 @@
settings.server.HTTP_PORT = 3002;
mirrors = {
adminUser = "admin";
adminEmail = "admin@bug.tools";
admin = {
user = "admin";
email = "admin@bug.tools";
};
repos = [
{ owner = "gmodena"; repo = "nix-flatpak"; source = "https://github.com/gmodena/nix-flatpak.git"; service = "github"; }
{ owner = "FlameFlag"; repo = "nixcord"; source = "https://github.com/FlameFlag/nixcord.git"; service = "github"; }
{ owner = "gmodena"; repo = "nix-flatpak"; service = "github"; }
{ owner = "FlameFlag"; repo = "nixcord"; service = "github"; }
{ owner = "jacob.eva"; repo = "opencom-lte"; source = "https://git.liberatedsystems.co.uk/jacob.eva/opencom-lte.git"; }
];
users = [
{ owner = "nix-community"; platform = "github"; }
{ owner = "catppuccin"; platform = "github"; }
{ owner = "picosh"; platform = "github"; }
{ owner = "vic"; platform = "github"; }

View File

@@ -1,4 +1,4 @@
{ den, ...}: {
{
den.aspects.glances = {
nixos = { pkgs, ... }: {
environment.systemPackages = [ pkgs.glances ];

View File

@@ -0,0 +1,189 @@
{ den, ... }: {
den.aspects.janitor-backend = {
nixos = { pkgs, lib, config, ... }:
let
cfg = config.services.janitor;
janitorScript = pkgs.writeText "janitor.py" ''
import os
import sys
import json
import shutil
import time
import logging
from pathlib import Path
logging.basicConfig(
level=logging.INFO,
format="[janitor] %(message)s",
stream=sys.stdout,
)
log = logging.getLogger("janitor")
CONFIG_PATH = os.environ.get("JANITOR_CONFIG")
def load_config():
if not CONFIG_PATH or not os.path.exists(CONFIG_PATH):
raise FileNotFoundError(f"JANITOR_CONFIG not set or missing: {CONFIG_PATH}")
with open(CONFIG_PATH) as f:
return json.load(f)
def get_destination(extension, rules):
ext = extension.lower().lstrip(".")
for folder, extensions in rules.items():
if ext in extensions:
return folder
return None
def resolve_dest(dest_key, watch_dir):
if os.path.isabs(dest_key):
return Path(dest_key)
return watch_dir.parent / dest_key
def unique_path(target):
if not target.exists():
return target
stem, suffix = target.stem, target.suffix
counter = 1
while True:
candidate = target.parent / f"{stem}_{counter}{suffix}"
if not candidate.exists():
return candidate
counter += 1
def main():
try:
config = load_config()
except Exception as e:
log.error("Failed to load config: %s", e)
sys.exit(1)
grace_period = config.get("grace_period", 60)
rules = config.get("rules", {})
now = time.time()
for watch_dir_str in config.get("watched_dirs", []):
watch_dir = Path(os.path.expanduser(watch_dir_str))
if not watch_dir.exists():
log.warning("Watched dir does not exist: %s", watch_dir)
continue
for item in watch_dir.iterdir():
if not item.is_file() or item.name.startswith("."):
continue
age = now - item.stat().st_mtime
if age < grace_period:
continue
dest_key = get_destination(item.suffix, rules)
if dest_key is None:
continue
target_dir = resolve_dest(dest_key, watch_dir)
target_dir.mkdir(parents=True, exist_ok=True)
target = unique_path(target_dir / item.name)
try:
shutil.move(str(item), str(target))
log.info("Moved %s -> %s", item.name, target)
except Exception as e:
log.error("Failed to move %s: %s", item.name, e)
if __name__ == "__main__":
main()
'';
janitorConfig = pkgs.writeText "janitor_config.json" (builtins.toJSON {
grace_period = cfg.gracePeriod;
watched_dirs = cfg.watchedDirs;
rules = cfg.rules;
});
in {
options.services.janitor = {
enable = lib.mkEnableOption "file sorting janitor";
interval = lib.mkOption {
type = lib.types.str;
default = "5min";
description = "How often to run the janitor (systemd time span, e.g. \"5min\", \"1h\").";
};
gracePeriod = lib.mkOption {
type = lib.types.int;
default = 60;
description = "Seconds a file must remain unmodified before it is eligible to be moved.";
};
watchedDirs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "~/Downloads" ];
description = "Directories to scan and sort. Supports ~ expansion.";
};
rules = lib.mkOption {
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
default = { };
description = ''
Mapping of destination folder path to a list of file extensions (without leading dot).
Destinations are relative to the parent of the watched directory, or absolute if they
start with /.
Example:
rules = {
"Pictures/Downloads" = [ "jpg" "png" "gif" "webp" ];
"Videos/Downloads" = [ "mp4" "mkv" "webm" ];
"/mnt/archive" = [ "zip" "tar" ];
};
'';
example = lib.literalExpression ''
{
"Pictures/Downloads" = [ "jpg" "jpeg" "png" "gif" "webp" "avif" ];
"Videos/Downloads" = [ "mp4" "mkv" "mov" "webm" "avi" ];
"Music/Downloads" = [ "mp3" "flac" "wav" "ogg" "opus" "m4a" ];
"Documents/Downloads" = [ "pdf" "doc" "docx" "odt" "txt" "md" "epub" ];
}
'';
};
user = lib.mkOption {
type = lib.types.str;
default = "bug";
description = "User account the janitor service runs as.";
};
};
config = lib.mkIf cfg.enable {
systemd.services.janitor = {
description = "File sorting janitor";
environment.JANITOR_CONFIG = "${janitorConfig}";
serviceConfig = {
Type = "oneshot";
User = cfg.user;
ExecStart = "${pkgs.python3}/bin/python3 ${janitorScript}";
};
};
systemd.timers.janitor = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "10m";
OnUnitActiveSec = cfg.interval;
};
};
};
};
};
}

View File

@@ -0,0 +1,39 @@
{ den, ... }: {
den.aspects.janitor = {
includes = [ den.aspects.janitor-backend ];
nixos = {
services.janitor = {
enable = true;
interval = "5min";
gracePeriod = 60;
watchedDirs = [ "~/Downloads" ];
rules = {
"Pictures/Downloads" = [ "jpg" "jpeg" "png" "gif" "webp" "svg" "heic" "avif" "ico" ];
"Videos/Downloads" = [ "mp4" "mkv" "mov" "webm" "avi" "flv" ];
"Music/Downloads" = [ "mp3" "flac" "wav" "ogg" "m4a" "opus" ];
"Documents/Downloads" = [ "pdf" "doc" "docx" "odt" "txt" "md" "epub" "ppt" "pptx" "xls" "xlsx" "csv" "iso" "zip" "tar" "gz" "bz2" "xz" "rar" "7z" ];
"Fonts/Downloads" = [ "ttf" "otf" "woff" "woff2" ];
"3D/Downloads" = [ "blend" "obj" "fbx" "stl" "dae" "3ds" "3mf" ];
# "Scripts/Downloads" = [ "sh" "py" "deb" "rpm" "appimage" "run" "jar" "exe" "msi" "lua" ];
"Games/Doom" = [ "wad" "pk3" ];
"Games/Switch" = [ "nsp" "xci" ];
"Games/3DS" = [ "3ds" "cia" ];
"Games/WiiU" = [ "wux" "wud" ];
"Games/Wii" = [ "wbfs" ];
"Games/GameCube" = [ "gcm" ];
"Games/N64" = [ "n64" "z64" ];
"Games/SNES" = [ "sfc" "smc" ];
"Games/NES" = [ "nes" ];
"Games/DS" = [ "nds" "dsi" ];
"Games/GBA" = [ "gba" ];
"Games/GBC" = [ "gbc" ];
"Games/GB" = [ "gb" ];
"Games/PS1" = [ "cue" "bin" ];
"Games/Genesis" = [ "gen" ];
"Games/Dreamcast" = [ "gdi" "cdi" ];
};
};
};
};
}

View File

@@ -10,7 +10,10 @@
vesktop.enable = true;
config = {
themeLinks = [ "https://catppuccin.github.io/discord/dist/catppuccin-mocha-mauve.theme.css" "https://codeberg.org/ridge/Discord-Adblock/raw/branch/main/discord-adblock.css" ];
themeLinks = [
"https://catppuccin.github.io/discord/dist/catppuccin-mocha-mauve.theme.css"
"https://codeberg.org/ridge/Discord-Adblock/raw/branch/main/discord-adblock.css"
];
plugins = {
alwaysTrust.enable = true;
@@ -38,6 +41,50 @@
viewIcons.enable = true;
volumeBooster.enable = true;
webScreenShareFixes.enable = true;
fixImagesQuality.enable = true;
messageLogger = {
enable = true;
collapseDeleted = true;
ignoreSelf = true;
ignoreBots = true;
};
textReplace.enable = true;
textReplace.regexRules = [
{
find = "https?:\\/\\/(www\\.)?instagram\\.com\\/[^\\/]+\\/(p|reel)\\/([A-Za-z0-9-_]+)\\/?";
replace = "https://g.ddinstagram.com/$2/$3";
}
{
find = "https:\\/\\/x\\.com\\/([^\\/]+\\/status\\/[0-9]+)";
replace = "https://vxtwitter.com/$1";
}
{
find = "https:\\/\\/twitter\\.com\\/([^\\/]+\\/status\\/[0-9]+)";
replace = "https://vxtwitter.com/$1";
}
{
find = "https:\\/\\/(www\\.|old\\.)?reddit\\.com\\/(r\\/[a-zA-Z0-9_]+\\/comments\\/[a-zA-Z0-9_]+\\/[^\\s]*)";
replace = "https://vxreddit.com/$2";
}
{
find = "https:\\/\\/(www\\.)?pixiv\\.net\\/(.*)";
replace = "https://phixiv.net/$2";
}
{
find = "https:\\/\\/(?:www\\.|m\\.)?twitch\\.tv\\/twitch\\/clip\\/(.*)";
replace = "https://clips.fxtwitch.tv/$1";
}
{
find = "https:\\/\\/(?:www\\.)?youtube\\.com\\/(?:watch\\?v=|shorts\\/)([a-zA-Z0-9_-]+)";
replace = "https://youtu.be/$1";
}
];
disableCallIdle.enable = true;
ClearURLs.enable = true;
};
};
};

View File

@@ -1,7 +0,0 @@
{ inputs, ... }: {
den.aspects.organize = {
nixos = {
};
};
}