summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--.dir-locals.el2
-rw-r--r--flake.lock88
-rw-r--r--flake.nix30
-rw-r--r--secrets/cifs-paperless.age7
-rw-r--r--secrets/cifs-photoprism.agebin0 -> 368 bytes
-rw-r--r--secrets/photoprism.age7
-rw-r--r--secrets/secrets.nix3
-rw-r--r--system/linde.nix162
-rw-r--r--system/marvin.nix2
-rw-r--r--user/emacs/init.el8
-rw-r--r--user/marvin.nix45
-rw-r--r--user/settings/darwin.nix13
-rw-r--r--user/settings/development/base.nix6
-rw-r--r--user/settings/emacs.nix2
-rw-r--r--user/settings/kitty.nix33
-rw-r--r--user/settings/user-interface.nix2
16 files changed, 271 insertions, 139 deletions
diff --git a/.dir-locals.el b/.dir-locals.el
index e352b5ac..3d3aa0bc 100644
--- a/.dir-locals.el
+++ b/.dir-locals.el
@@ -1,4 +1,4 @@
 ;;; Directory Local Variables
 ;;; For more information see (info "(emacs) Directory Variables")
 
-((nil . ((compile-command . "./bin/home-manager switch"))))
+((nil . ((compile-command . "home-manager switch --flake ."))))
diff --git a/flake.lock b/flake.lock
index 4a9c0535..0172b702 100644
--- a/flake.lock
+++ b/flake.lock
@@ -52,11 +52,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1733570843,
-        "narHash": "sha256-sQJAxY1TYWD1UyibN/FnN97paTFuwBw3Vp3DNCyKsMk=",
+        "lastModified": 1735478292,
+        "narHash": "sha256-Ys9pSP9ch0SthhpbjnkCSJ9ZLfaNKnt/dcy7swjmS1A=",
         "owner": "lnl7",
         "repo": "nix-darwin",
-        "rev": "a35b08d09efda83625bef267eb24347b446c80b8",
+        "rev": "71a3a075e3229a7518d76636bb762aef2bcb73ac",
         "type": "github"
       },
       "original": {
@@ -113,11 +113,11 @@
         "nixpkgs-stable": "nixpkgs-stable"
       },
       "locked": {
-        "lastModified": 1734944495,
-        "narHash": "sha256-pAW9SbQSJmL2ntnCfeSVT+S9078ErqVztKS1lrKLOhk=",
+        "lastModified": 1735550039,
+        "narHash": "sha256-hIyQM5hqBpOfvb6lMHl+707pg7iwBJKfbsANEZFhV+0=",
         "owner": "nix-community",
         "repo": "emacs-overlay",
-        "rev": "49bd3fd75db9c063076c4b572778be5c96899570",
+        "rev": "bc19dc80cd2987406a19b5c644e0400c4cf67e33",
         "type": "github"
       },
       "original": {
@@ -241,7 +241,7 @@
       "inputs": {
         "flake-utils": "flake-utils",
         "nixpkgs": [
-          "nixpkgs-small"
+          "nixpkgs"
         ]
       },
       "locked": {
@@ -311,11 +311,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1734944412,
-        "narHash": "sha256-36QfCAl8V6nMIRUCgiC79VriJPUXXkHuR8zQA1vAtSU=",
+        "lastModified": 1735381016,
+        "narHash": "sha256-CyCZFhMUkuYbSD6bxB/r43EdmDE7hYeZZPTCv0GudO4=",
         "owner": "nix-community",
         "repo": "home-manager",
-        "rev": "8264bfe3a064d704c57df91e34b795b6ac7bad9e",
+        "rev": "10e99c43cdf4a0713b4e81d90691d22c6a58bdf2",
         "type": "github"
       },
       "original": {
@@ -351,11 +351,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1734838217,
-        "narHash": "sha256-zvMLS8BGn+kMG7tLLT3PJ67/S9yqZ9B7V8hKBa9cRRY=",
+        "lastModified": 1735443188,
+        "narHash": "sha256-AydPpRBh8+NOkrLylG7vTsHrGO2b5L7XkMEL5HlzcA8=",
         "owner": "Mic92",
         "repo": "nix-index-database",
-        "rev": "d583b2d142f0428313df099f4a2dcf2a0496aa78",
+        "rev": "55ab1e1df5daf2476e6b826b69a82862dcbd7544",
         "type": "github"
       },
       "original": {
@@ -366,11 +366,11 @@
     },
     "nixos-hardware": {
       "locked": {
-        "lastModified": 1734954597,
-        "narHash": "sha256-QIhd8/0x30gEv8XEE1iAnrdMlKuQ0EzthfDR7Hwl+fk=",
+        "lastModified": 1735388221,
+        "narHash": "sha256-e5IOgjQf0SZcFCEV/gMGrsI0gCJyqOKShBQU0iiM3Kg=",
         "owner": "NixOS",
         "repo": "nixos-hardware",
-        "rev": "def1d472c832d77885f174089b0d34854b007198",
+        "rev": "7c674c6734f61157e321db595dbfcd8523e04e19",
         "type": "github"
       },
       "original": {
@@ -397,11 +397,11 @@
     },
     "nixpkgs-small": {
       "locked": {
-        "lastModified": 1735268880,
-        "narHash": "sha256-7QEFnKkzD13SPxs+UFR5bUFN2fRw+GlL0am72ZjNre4=",
+        "lastModified": 1735530358,
+        "narHash": "sha256-4ZbiXBWFK0gHsl5VT9dih7RVaEV3rRh0XUV0jW0ibOM=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "7cc0bff31a3a705d3ac4fdceb030a17239412210",
+        "rev": "5000219208d860bafd1ee26eadb403449f3d9ab9",
         "type": "github"
       },
       "original": {
@@ -413,11 +413,11 @@
     },
     "nixpkgs-stable": {
       "locked": {
-        "lastModified": 1734737257,
-        "narHash": "sha256-GIMyMt1pkkoXdCq9un859bX6YQZ/iYtukb9R5luazLM=",
+        "lastModified": 1735412871,
+        "narHash": "sha256-Qoz0ow6jDGUIBHxduc7Y1cjYFS71tvEGJV5Src/mj98=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "1c6e20d41d6a9c1d737945962160e8571df55daa",
+        "rev": "9f94733f93e4fe6e82f516efae007096e4ab5a21",
         "type": "github"
       },
       "original": {
@@ -445,11 +445,11 @@
     },
     "nixpkgs_2": {
       "locked": {
-        "lastModified": 1734649271,
-        "narHash": "sha256-4EVBRhOjMDuGtMaofAIqzJbg4Ql7Ai0PSeuVZTHjyKQ=",
+        "lastModified": 1735471104,
+        "narHash": "sha256-0q9NGQySwDQc7RhAV2ukfnu7Gxa5/ybJ2ANT8DQrQrs=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "d70bd19e0a38ad4790d3913bf08fcbfc9eeca507",
+        "rev": "88195a94f390381c6afcdaa933c2f6ff93959cb4",
         "type": "github"
       },
       "original": {
@@ -477,11 +477,11 @@
     },
     "nixpkgs_4": {
       "locked": {
-        "lastModified": 1735291276,
-        "narHash": "sha256-NYVcA06+blsLG6wpAbSPTCyLvxD/92Hy4vlY9WxFI1M=",
+        "lastModified": 1735471104,
+        "narHash": "sha256-0q9NGQySwDQc7RhAV2ukfnu7Gxa5/ybJ2ANT8DQrQrs=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "634fd46801442d760e09493a794c4f15db2d0cbb",
+        "rev": "88195a94f390381c6afcdaa933c2f6ff93959cb4",
         "type": "github"
       },
       "original": {
@@ -523,23 +523,6 @@
         "type": "github"
       }
     },
-    "personal": {
-      "inputs": {
-        "nixpkgs": [
-          "nixpkgs"
-        ]
-      },
-      "locked": {
-        "lastModified": 1734037976,
-        "narHash": "sha256-gOd0dQcweFFQW2QA7LPxzKAQw+p4yOiNc/WiTNu9jwo=",
-        "path": "/home/alan/projects/alanpearce.eu/nixfiles/packages",
-        "type": "path"
-      },
-      "original": {
-        "type": "git",
-        "url": "file:packages"
-      }
-    },
     "pre-commit-hooks": {
       "inputs": {
         "flake-compat": "flake-compat_2",
@@ -574,9 +557,7 @@
         "nixos-hardware": "nixos-hardware",
         "nixpkgs": "nixpkgs_4",
         "nixpkgs-small": "nixpkgs-small",
-        "personal": "personal",
         "searchix": "searchix",
-        "secrets": "secrets",
         "utils": "utils_2"
       }
     },
@@ -602,19 +583,6 @@
         "url": "https://git.alanpearce.eu/searchix"
       }
     },
-    "secrets": {
-      "flake": false,
-      "locked": {
-        "lastModified": 1735315234,
-        "narHash": "sha256-YfHf6RCPBc9XhH7RHheSyXemjawm9aQ/zIgnNXtsK+U=",
-        "path": "/home/alan/projects/alanpearce.eu/nixfiles/private",
-        "type": "path"
-      },
-      "original": {
-        "type": "git",
-        "url": "file:private"
-      }
-    },
     "simple-css": {
       "flake": false,
       "locked": {
diff --git a/flake.nix b/flake.nix
index 5333f1a2..99cbdd93 100644
--- a/flake.nix
+++ b/flake.nix
@@ -11,22 +11,14 @@
     home-manager.url = "github:nix-community/home-manager";
     home-manager.inputs.nixpkgs.follows = "nixpkgs";
     nh-darwin.url = "github:ToyVo/nh_darwin";
-    secrets = {
-      flake = false;
-      url = "git+file:private";
-    };
     utils.url = "github:numtide/flake-utils";
     agenix.url = "github:ryantm/agenix";
     agenix.inputs.nixpkgs.follows = "nixpkgs";
     deploy-rs.url = "github:serokell/deploy-rs";
-    personal = {
-      url = "git+file:packages";
-      inputs.nixpkgs.follows = "nixpkgs";
-    };
     searchix.url = "git+https://git.alanpearce.eu/searchix";
     golink = {
       url = "github:tailscale/golink";
-      inputs.nixpkgs.follows = "nixpkgs-small";
+      inputs.nixpkgs.follows = "nixpkgs";
     };
   };
 
@@ -42,9 +34,7 @@
     , darwin
     , nh-darwin
     , nix-index-database
-    , secrets
     , agenix
-    , personal
     , deploy-rs
     , searchix
     , golink
@@ -69,7 +59,7 @@
             agenix.overlays.default
             emacs-overlay.overlays.default
             (self: super: {
-              personal = personal.packages.${system};
+              personal = import ./packages/overlay.nix self super;
               enchant = super.enchant.override {
                 withHspell = false;
                 withAspell = false;
@@ -114,7 +104,7 @@
           agenix.nixosModules.default
           searchix.nixosModules.web
           golink.nixosModules.default
-          personal.nixosModules.laminar
+          ./packages/modules/nixos/laminar.nix
           ./system/linde.nix
         ];
       };
@@ -123,42 +113,42 @@
         specialArgs = { inherit inputs; };
         modules = [
           ./system/marvin.nix
+          ./packages/modules/darwin/caddy
           nh-darwin.nixDarwinModules.prebuiltin
-          personal.darwinModules.caddy
         ];
       };
       homeConfigurations."alan@marvin" = mkHomeConfiguration {
         system = utils.lib.system.aarch64-darwin;
         modules = [
           ./user/marvin.nix
+          ./private/default.nix
+          ./private/ssh.nix
           nix-index-database.hmModules.nix-index
-          (secrets + "/default.nix")
-          (secrets + "/ssh.nix")
         ];
       };
       homeConfigurations."alan@prefect" = mkHomeConfiguration {
         system = utils.lib.system.x86_64-linux;
         modules = [
           ./user/prefect.nix
+          ./private/default.nix
+          ./private/ssh.nix
           nix-index-database.hmModules.nix-index
-          (secrets + "/default.nix")
-          (secrets + "/ssh.nix")
         ];
       };
       homeConfigurations."alan@nanopi" = mkHomeConfiguration {
         system = utils.lib.system.aarch64-linux;
         modules = [
           ./user/nanopi.nix
+          ./private/default.nix
           nix-index-database.hmModules.nix-index
-          (secrets + "/default.nix")
         ];
       };
       homeConfigurations."alan@linde" = mkHomeConfiguration {
         system = utils.lib.system.aarch64-linux;
         modules = [
           ./user/server.nix
+          ./private/default.nix
           nix-index-database.hmModules.nix-index
-          (secrets + "/default.nix")
         ];
       };
 
diff --git a/secrets/cifs-paperless.age b/secrets/cifs-paperless.age
new file mode 100644
index 00000000..8a510314
--- /dev/null
+++ b/secrets/cifs-paperless.age
@@ -0,0 +1,7 @@
+age-encryption.org/v1
+-> ssh-ed25519 cvV2sw m5nrwSF0dhp432vdyHt9qn9VU46NwF5uoILs2uKRyG4
+9QBfJXCjT9BBMIzou/oweWhenkYmP9q2whE8G6Q+15Q
+-> piv-p256 VBDKjg A2aESbZZG7090wQqjU8IljN+G+Gja5MpIeYdcsS4eyqC
+Ow0/HkI2/Wp1sTQyhmDfVRD6yaufkHiNX+nuzQhywhw
+--- CNUUxOYG0GgCGlZ7mDkBltwsynq7OhEY5vBxmwg6l74
+J1ӣkmEkMJO*4<#1)͖scla$*%<z^NQT1\xEZU$
\ No newline at end of file
diff --git a/secrets/cifs-photoprism.age b/secrets/cifs-photoprism.age
new file mode 100644
index 00000000..cab56f39
--- /dev/null
+++ b/secrets/cifs-photoprism.age
Binary files differdiff --git a/secrets/photoprism.age b/secrets/photoprism.age
new file mode 100644
index 00000000..7b6534fa
--- /dev/null
+++ b/secrets/photoprism.age
@@ -0,0 +1,7 @@
+age-encryption.org/v1
+-> ssh-ed25519 cvV2sw n4h/PGlbNj9UGICFTdf94svZOLL2uCrtYrmRVCgquC0
+w8sZ6j2n/xlPW1KmIESNehy5M6xXzuRiYb4fWNk9bZo
+-> piv-p256 VBDKjg AvdZP758E9FCgQNfaMEH2BhPjHtZOe2hVLN008cZYeID
+jPxUhgwOLnO3ioVvinqDHVwYYWi88zH+1VoJn4lTOx8
+--- p7T2ZkbKKr7yewVtqnzYvb/9Nw06mMZZrsQaXQmT1Ts
+R^`bӂ-ѵ[dGr:4R^Gy*}t'^/(<g";#.:CA
\ No newline at end of file
diff --git a/secrets/secrets.nix b/secrets/secrets.nix
index 3cfcf017..a4b464da 100644
--- a/secrets/secrets.nix
+++ b/secrets/secrets.nix
@@ -20,6 +20,9 @@ let
     powerdns = [ linde ];
     dex = [ linde ];
     golink = [ linde ];
+    photoprism = [ linde ];
+    cifs-photoprism = [ linde ];
+    cifs-paperless = [ linde ];
 
     dyndns = [ nanopi ];
     syncthing = [ nanopi ];
diff --git a/system/linde.nix b/system/linde.nix
index 2ca59842..da353bde 100644
--- a/system/linde.nix
+++ b/system/linde.nix
@@ -45,6 +45,9 @@ in
     dex.file = ../secrets/dex.age;
     powerdns.file = ../secrets/powerdns.age;
     redis-website.file = ../secrets/redis-website.age;
+    photoprism.file = ../secrets/photoprism.age;
+    cifs-photoprism.file = ../secrets/cifs-photoprism.age;
+    cifs-paperless.file = ../secrets/cifs-paperless.age;
     golink = let golink = config.services.golink; in {
       # hope this doesn't collide...
       path = "${golink.dataDir}/.config/tsnet-golink/auth.key";
@@ -64,10 +67,10 @@ in
 
   i18n.defaultLocale = "en_GB.UTF-8";
 
-  environment.enableAllTerminfo = true;
   environment.homeBinInPath = true;
   environment.localBinInPath = true;
   environment.systemPackages = with pkgs; [
+    cifs-utils
     htop
     lsof
     powerdns
@@ -99,14 +102,14 @@ in
     dates = "02:10";
     randomizedDelaySec = "59 min";
     allowReboot = true;
-    flake = "git+file://${config.services.gitolite.dataDir}/repositories/nixfiles.git";
+    flake = "git+file://${config.services.gitolite.dataDir}/repositories/nixfiles.git?submodules=1";
     flags = [
       "--no-write-lock-file"
       "--impure"
       "--update-input"
-      "nixpkgs-small"
+      "--nixpkgs"
       "--update-input"
-      "searchix"
+      "nixpkgs-small"
     ];
   };
 
@@ -152,8 +155,8 @@ in
       "1.0.0.1"
     ];
     hosts = lib.mkForce {
-      ${net-ip4} = [ "${hostname}.alanpearce.eu" hostname ];
-      ${net-ip6} = [ "${hostname}.alanpearce.eu" hostname ];
+      ${net-ip4} = [ "${hostname}.${domain}" hostname ];
+      ${net-ip6} = [ "${hostname}.${domain}" hostname ];
       ${net-rdnsip} = [ "dns" ];
       ${net-redisip} = [ "redis" ];
     };
@@ -297,7 +300,6 @@ in
       set --universal fish_greeting ""
     '';
   };
-  programs.zsh.enable = true;
   users.users.root = {
     shell = "/run/current-system/sw/bin/fish";
     openssh.authorizedKeys.keys = [
@@ -582,8 +584,8 @@ in
       openssh
     ];
     script = ''
-      rdiff-backup --api-version 201 backup ${config.services.gitolite.dataDir} ${hostname}@home.alanpearce.eu::gitolite
-      rdiff-backup --api-version 201 remove increments --older-than 3M ${hostname}@home.alanpearce.eu::gitolite
+      rdiff-backup --api-version 201 backup ${config.services.gitolite.dataDir} ${hostname}@nano.${ts-domain}::gitolite
+      rdiff-backup --api-version 201 remove increments --older-than 3M ${hostname}@nano.${ts-domain}::gitolite
     '';
     serviceConfig.Type = "oneshot";
   };
@@ -596,9 +598,9 @@ in
       openssh
     ];
     script = ''
-      sudo -u paperless ./paperless-manage document_exporter --delete --use-filename-format --no-archive --no-thumbnail --no-progress-bar ./export
-      rdiff-backup --api-version 201 backup ./export ${hostname}@home.alanpearce.eu::paperless
-      rdiff-backup --api-version 201 remove increments --older-than 3M ${hostname}@home.alanpearce.eu::paperless
+      systemd-run --machine=papers sudo -u paperless ./paperless-manage document_exporter --delete --use-filename-format --no-archive --no-thumbnail --no-progress-bar ./export
+      rdiff-backup --api-version 201 backup /srv/paperless/export ${hostname}@nano.${ts-domain}::paperless
+      rdiff-backup --api-version 201 remove increments --older-than 3M ${hostname}@nano.${ts-domain}::paperless
     '';
     serviceConfig = {
       Type = "oneshot";
@@ -783,6 +785,21 @@ in
             file_server
           '';
         };
+        "photos.alanpearce.eu" =
+          let
+            srv = config.services.photoprism;
+          in
+          {
+            useACMEHost = "alanpearce.eu";
+            extraConfig = ''
+              encode zstd gzip
+              ${security-headers {}}
+              reverse_proxy ${srv.address}:${toString srv.port}
+              handle_errors {
+                respond "{err.status_code} {err.status_text}"
+              }
+            '';
+          };
       };
   };
   systemd.services.caddy.serviceConfig = {
@@ -802,15 +819,41 @@ in
     home = "/srv/paperless";
   };
   users.groups.paperless.members = [ "alan" "syncthing" ];
+
+  fileSystems."/srv/paperless" = {
+    device = "//u439959-sub3.your-storagebox.de/u439959-sub3";
+    fsType = "smb3";
+    options =
+      let
+        # prevents hanging on network split
+        automount_opts = [
+          "x-systemd.automount"
+          "noauto"
+          "x-systemd.idle-timeout=1h"
+          "x-systemd.mount-timeout=5s"
+        ];
+        uid = config.ids.uids.paperless;
+      in
+      automount_opts ++ [
+        "credentials=${config.age.secrets.cifs-paperless.path}"
+        "seal"
+        "multichannel"
+        "nobrl" # needed for sqlite
+        "forceuid"
+        "forcegid"
+        "uid=${toString uid}"
+        "gid=${toString uid}"
+      ];
+  };
   containers.papers =
     let
-      hostDataDir = config.users.users.paperless.home;
+      externalDir = "/srv/paperless";
       localAddress6 = "fc00::2";
       tsHostname = "papers.${ts-domain}";
       tsPort = 41642;
+      hostConfig = config;
     in
     {
-      # or maybe socket activated?
       autoStart = true;
       # does TS need this?
       enableTun = true;
@@ -822,11 +865,15 @@ in
       }];
       bindMounts = {
         ${config.services.paperless.dataDir} = {
-          hostPath = hostDataDir;
+          hostPath = hostConfig.services.paperless.dataDir;
+          isReadOnly = false;
+        };
+        ${externalDir} = {
+          hostPath = externalDir;
           isReadOnly = false;
         };
       };
-      config = {
+      config = { config, lib, pkgs, ... }: {
         environment.systemPackages = with pkgs; [
           lsof
         ];
@@ -835,7 +882,7 @@ in
           resolvconf.enable = false;
           firewall.trustedInterfaces = [ "tailscale0" ];
           firewall.rejectPackets = true;
-          nameservers = config.networking.nameservers;
+          nameservers = hostConfig.networking.nameservers;
         };
         services.resolved = {
           enable = true;
@@ -894,6 +941,7 @@ in
         services.paperless = {
           enable = true;
           address = "[::1]";
+          mediaDir = "${externalDir}/media";
           settings = {
             PAPERLESS_DBENGINE = "sqlite";
             PAPERLESS_TIME_ZONE = "Europe/Berlin";
@@ -926,15 +974,13 @@ in
       };
     };
 
-  services.etcd = {
-    enable = true;
-    initialClusterState = "existing";
-    dataDir = "/var/lib/etcd"; # TODO backup
-    extraConf = {
-      AUTO_COMPACTION_RETENTION = "1h";
-    };
+  users.users.dex = {
+    home = "/var/lib/dex";
+    createHome = true;
+    isSystemUser = true;
+    group = "dex";
   };
-
+  users.groups.dex = { };
   services.dex =
     let
       issuer = "https://id.alanpearce.eu/";
@@ -945,11 +991,8 @@ in
       settings = {
         inherit issuer;
         storage = {
-          type = "etcd";
-          config = {
-            endpoints = config.services.etcd.listenClientUrls;
-            namespace = "dex/";
-          };
+          type = "sqlite3";
+          config.file = "/var/lib/dex/storage.sqlite";
         };
         web.http = "127.0.0.1:5556";
         connectors = [{
@@ -977,10 +1020,16 @@ in
         ];
       };
     };
-  systemd.services.dex.unitConfig = {
-    After = [ "etcd.service" ];
-    Requires = [ "etcd.service" ];
-  };
+  systemd.services.dex.serviceConfig =
+    let
+      user = config.users.users.dex;
+    in
+    {
+      ReadWritePaths = [ user.home ];
+      DynamicUser = lib.mkForce false;
+      User = user.name;
+      Group = user.group;
+    };
 
   services.redis = {
     servers = {
@@ -1124,4 +1173,49 @@ in
       default = [{ type = "insecureAcceptAnything"; }];
     };
   };
+
+  fileSystems."/srv/photoprism" = {
+    device = "//u439959-sub1.your-storagebox.de/u439959-sub1";
+    fsType = "smb3";
+    options =
+      let
+        # prevents hanging on network split
+        automount_opts = [
+          "x-systemd.automount"
+          "noauto"
+          "x-systemd.idle-timeout=1h"
+          "x-systemd.mount-timeout=5s"
+        ];
+        uid = 64600;
+      in
+      automount_opts ++ [
+        "credentials=${config.age.secrets.cifs-photoprism.path}"
+        "seal"
+        "multichannel"
+        "nobrl" # needed for sqlite
+        "forceuid"
+        "forcegid"
+        "uid=${toString uid}"
+        "gid=${toString uid}"
+      ];
+  };
+  services.photoprism = {
+    enable = true;
+    passwordFile = config.age.secrets.photoprism.path;
+    originalsPath = "/srv/photoprism/originals";
+    importPath = "/srv/photoprism/import";
+    settings = {
+      PHOTOPRISM_SITE_URL = "https://photos.alanpearce.eu";
+      PHOTOPRISM_SITE_CAPTION = "Alan‘s Photos";
+      PHOTOPRISM_DISABLE_TLS = "true";
+      PHOTOPRISM_SIDECAR_PATH = "/srv/photoprism/sidecar";
+      PHOTOPRISM_SPONSOR = "true";
+    };
+  };
+  systemd.services.photoprism = {
+    unitConfig.RequiresMountsFor = "/srv/photoprism";
+    serviceConfig.ReadWritePaths = [
+      "/srv/photoprism/sidecar"
+    ];
+  };
 }
diff --git a/system/marvin.nix b/system/marvin.nix
index 2dd7986a..c4a13e93 100644
--- a/system/marvin.nix
+++ b/system/marvin.nix
@@ -53,7 +53,7 @@
         system = "aarch64-linux";
         maxJobs = 2;
         speedFactor = 1;
-        supportedFeatures = [ ];
+        supportedFeatures = [ "kvm" ];
       }
     ];
   };
diff --git a/user/emacs/init.el b/user/emacs/init.el
index 56a437bb..037f8385 100644
--- a/user/emacs/init.el
+++ b/user/emacs/init.el
@@ -31,6 +31,8 @@
   (interactive)
   (load-file user-init-file))
 
+(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
+
 (setq use-package-enable-imenu-support t)
 (require 'use-package)
 (setq use-package-always-demand (daemonp)
@@ -788,6 +790,12 @@ _C-k_: prev  _u_pper              _=_: upper/lower       _s_mart resolve
   :general (:keymaps 'comint-mode-map
                      "C-c C-l" #'counsel-shell-history))
 
+(use-package chatgpt-shell
+  :defer 5
+  :config (progn
+            (chatgpt-shell-ollama-load-models :override t)
+            (setq chatgpt-shell-model-version "llama3.3")))
+
 ;;; Editing
 
 (setq-default tab-always-indent 'complete
diff --git a/user/marvin.nix b/user/marvin.nix
index 41cab689..f6a745c6 100644
--- a/user/marvin.nix
+++ b/user/marvin.nix
@@ -1,4 +1,4 @@
-{ pkgs, ... }: {
+{ config, pkgs, ... }: {
   imports = [
     ./settings/base.nix
     ./settings/development/base.nix
@@ -9,7 +9,6 @@
     ./settings/emacs.nix
     ./settings/fish.nix
     ./settings/git.nix
-    ./settings/kitty.nix
     ./settings/nixpkgs.nix
     ./settings/ssh.nix
     ./settings/tabnine.nix
@@ -20,24 +19,56 @@
   home.username = "alan";
   home.homeDirectory = "/Users/alan";
   home.stateVersion = "22.11";
-  home.sessionPath = [
-    "$HOME/.cache/lm-studio/bin"
-  ];
   home.packages = with pkgs; [
     picocom
+    ollama
   ];
 
+  launchd.agents = {
+    ollama = {
+      enable = true;
+      config = {
+        ProgramArguments = [ "/Users/alan/.local/state/nix/profile/bin/ollama" "serve" ];
+        RunAtLoad = true;
+        KeepAlive = true;
+        WorkingDirectory = "/Users/alan";
+        EnvironmentVariables = {
+          OLLAMA_HOST = "[::]:11434";
+          OLLAMA_KEEP_ALIVE = "-1"; # keep models in memory forever
+          OLLAMA_FLASH_ATTENTION = "1"; # significantly reduce memory usage as the context size grows
+        };
+      };
+    };
+    ollama-preload = {
+      enable = true;
+      config = {
+        ProgramArguments = [
+          "/Users/alan/.local/state/nix/profile/bin/ollama"
+          "run"
+          "llama3.3"
+          ""
+        ];
+        RunAtLoad = true;
+        KeepAlive = false;
+        WorkingDirectory = "/Users/alan";
+      };
+    };
+  };
+
   launchd.agents.colima = {
     enable = true;
     config = {
-      ProgramArguments = [ "${pkgs.colima}/bin/colima" "start" ];
+      ProgramArguments = [ "/Users/alan/.local/state/nix/profile/bin/colima" "start" ];
       RunAtLoad = true;
       # It doesn't run in the foreground, yet...
       # KeepAlive = true;
       WorkingDirectory = "/Users/alan";
       StandardOutPath = "/Users/alan/Library/Logs/colima.log";
       StandardErrorPath = "/Users/alan/Library/Logs/colima.log";
-      EnvironmentVariables.HOME = "/Users/alan";
+      EnvironmentVariables = {
+        HOME = "/Users/alan";
+        XDG_CONFIG_HOME = config.xdg.configHome;
+      };
     };
   };
 
diff --git a/user/settings/darwin.nix b/user/settings/darwin.nix
index 1ea3d470..1c0f6a74 100644
--- a/user/settings/darwin.nix
+++ b/user/settings/darwin.nix
@@ -66,6 +66,7 @@
               (
                 pkgs.writeShellScript
                   "toggle-dark-light-mode"
+                  (
                   ''
                     wait4path /nix
                     if defaults read -g AppleInterfaceStyle &>/dev/null ; then
@@ -74,7 +75,6 @@
                       MODE="light"
                     fi
                     emacsclient="${config.programs.emacs.finalPackage}/bin/emacsclient"
-                    kitty="${pkgs.kitty}/bin/kitty +kitten themes --config-file-name=theme.conf --reload-in=all --cache-age=-1"
                     emacsSwitchTheme () {
                       if pgrep -q Emacs; then
                         if [[  $MODE == "dark"  ]]; then
@@ -88,18 +88,9 @@
                         fi
                       fi
                     }
-                    kittySwitchTheme () {
-                      if pgrep -q kitty; then
-                        if [[  $MODE == "dark"  ]]; then
-                          $kitty 'Modus Vivendi'
-                        elif [[ $MODE == "light" ]]; then
-                          $kitty 'Modus Operandi'
-                        fi
-                      fi
-                    }
                     emacsSwitchTheme
-                    kittySwitchTheme
                   ''
+                  )
               )
           )
         ];
diff --git a/user/settings/development/base.nix b/user/settings/development/base.nix
index b0d23147..a454bc11 100644
--- a/user/settings/development/base.nix
+++ b/user/settings/development/base.nix
@@ -27,17 +27,17 @@
       watchexec
       entr
 
+      litecli
+
       diffoscopeMinimal
 
       skopeo
       docker-credential-helpers
       dive
-    ] ++ (if stdenv.isDarwin then [
+    ] ++ (lib.optionals stdenv.isDarwin [
       lima
       colima
       docker-client
-    ] else [
-      httping
     ]);
 
   home.sessionVariables = {
diff --git a/user/settings/emacs.nix b/user/settings/emacs.nix
index 73a3b55a..f0a0a0ee 100644
--- a/user/settings/emacs.nix
+++ b/user/settings/emacs.nix
@@ -74,6 +74,7 @@ in
         cape
         clojure-mode
         cask-mode
+        chatgpt-shell
         corfu
         consult
         consult-dir
@@ -156,6 +157,7 @@ in
         treemacs-nerd-icons
         treesit-grammars.with-all-grammars
         treesit-auto
+        try
         vc-msg
         vertico
         vertico-prescient
diff --git a/user/settings/kitty.nix b/user/settings/kitty.nix
index 8a06a820..c80c5fbd 100644
--- a/user/settings/kitty.nix
+++ b/user/settings/kitty.nix
@@ -1,4 +1,5 @@
-{ pkgs
+{ config
+, pkgs
 , ...
 }:
 {
@@ -24,4 +25,34 @@
       include ~/.config/kitty/theme.conf
     '';
   };
+  launchd.agents.kitty-dark-light = {
+    enable = true;
+    config = {
+      WatchPaths = [ "${config.home.homeDirectory}/Library/Preferences/.GlobalPreferences.plist" ];
+      StandardOutputPath = "/dev/null";
+      StandardErrorPath = "/dev/null";
+      RunAtLoad = true;
+      KeepAlive = false;
+      ProgramArguments = [
+        "/bin/sh"
+        (toString (pkgs.writeShellScript "toggle-dark-light-mode" ''
+          wait4path /nix
+          if defaults read -g AppleInterfaceStyle &>/dev/null ; then
+            MODE="dark"
+          else
+            MODE="light"
+          fi
+          kitty="${pkgs.kitty}/bin/kitty +kitten themes --config-file-name=theme.conf --reload-in=all --cache-age=-1"
+          if pgrep -q kitty; then
+            if [[  $MODE == "dark"  ]]; then
+              $kitty 'Modus Vivendi'
+            elif [[ $MODE == "light" ]]; then
+              $kitty 'Modus Operandi'
+            fi
+          fi
+        ''
+        ))
+      ];
+    };
+  };
 }
diff --git a/user/settings/user-interface.nix b/user/settings/user-interface.nix
index 0bf59af9..70c9392e 100644
--- a/user/settings/user-interface.nix
+++ b/user/settings/user-interface.nix
@@ -7,7 +7,7 @@ let
 in
 {
   imports = [
-    ./kitty.nix
+    # ./kitty.nix
   ];
 
   services.ssh-agent = {