diff --git a/.stfolder/syncthing-folder-a0745f.txt b/.stfolder/syncthing-folder-a0745f.txt index b3ae570..2f6a7df 100644 --- a/.stfolder/syncthing-folder-a0745f.txt +++ b/.stfolder/syncthing-folder-a0745f.txt @@ -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 diff --git a/README.md b/README.md index 1882194..1fd6ce2 100644 --- a/README.md +++ b/README.md @@ -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 | \ No newline at end of file +## 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 | diff --git a/modules/services/gitea-mirrors.nix b/modules/services/gitea-mirrors.nix index 2617fbe..c105dc1 100644 --- a/modules/services/gitea-mirrors.nix +++ b/modules/services/gitea-mirrors.nix @@ -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"; + }; + }; }; }; }; diff --git a/modules/services/gitea.nix b/modules/services/gitea.nix index 6281886..4053ba5 100644 --- a/modules/services/gitea.nix +++ b/modules/services/gitea.nix @@ -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"; } diff --git a/modules/services/glances.nix b/modules/services/glances.nix index f510c81..7c67e9b 100644 --- a/modules/services/glances.nix +++ b/modules/services/glances.nix @@ -1,4 +1,4 @@ -{ den, ...}: { +{ den.aspects.glances = { nixos = { pkgs, ... }: { environment.systemPackages = [ pkgs.glances ]; diff --git a/modules/software/janitor-backend.nix b/modules/software/janitor-backend.nix new file mode 100644 index 0000000..f4adb54 --- /dev/null +++ b/modules/software/janitor-backend.nix @@ -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; + }; + }; + }; + }; + }; +} \ No newline at end of file diff --git a/modules/software/janitor.nix b/modules/software/janitor.nix new file mode 100644 index 0000000..03e8eb5 --- /dev/null +++ b/modules/software/janitor.nix @@ -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" ]; + }; + }; + }; + }; +} diff --git a/modules/software/nixcord.nix b/modules/software/nixcord.nix index 421ab9c..fb44118 100644 --- a/modules/software/nixcord.nix +++ b/modules/software/nixcord.nix @@ -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; }; }; }; diff --git a/modules/software/organize.nix b/modules/software/organize.nix deleted file mode 100644 index 0e02595..0000000 --- a/modules/software/organize.nix +++ /dev/null @@ -1,7 +0,0 @@ -{ inputs, ... }: { - den.aspects.organize = { - nixos = { - - }; - }; -}