Commit 07d8739

HPCesia <me@hpcesia.com>
2026-08-05 14:21:26
desktop: init
Assisted-by: opencode:deepseek-v4-flash
1 parent e33af02
modules/desktop/app/browser/floorp/_hjem-module.nix
@@ -0,0 +1,539 @@
+# Port from home-manager's programs.floorp
+# See https://github.com/nix-community/home-manager/tree/c53d643b3737e2fcd04e6cb3b3580ef50b2087a0/modules/programs/firefox
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}: let
+  inherit (lib) concatStrings mapAttrsToList mkIf mkMerge mkOption optionalAttrs;
+  inherit (lib.types) attrsOf bool ints listOf nullOr package path str submodule;
+
+  modulePath = ["programs" "floorp"];
+  cfg = config.programs.floorp;
+
+  appName = "Floorp";
+  configPath = ".floorp";
+  extensionPath = "extensions/{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
+
+  jsonFormat = pkgs.formats.json {};
+
+  userPrefValue = pref:
+    builtins.toJSON (
+      if lib.isBool pref || lib.isInt pref || lib.isString pref || lib.isPath pref
+      then pref
+      else builtins.toJSON pref
+    );
+
+  extensionSettingsNeedForce = extensionSettings:
+    builtins.any (ext: ext.settings != {}) (builtins.attrValues extensionSettings);
+
+  extensionSettingsMissingForce = extensionSettings:
+    builtins.any (ext: ext.settings != {} && !ext.force) (builtins.attrValues extensionSettings);
+
+  mkUserJs = prePrefs: prefs: extraPrefs: extensions: let
+    prefs' =
+      optionalAttrs (extensionSettingsNeedForce extensions) {
+        "extensions.webextensions.ExtensionStorageIDB.enabled" = false;
+      }
+      // prefs;
+  in ''
+    // Generated by Hjem.
+
+    ${prePrefs}
+
+    ${concatStrings (
+      mapAttrsToList (name: value: ''
+        user_pref("${name}", ${userPrefValue value});
+      '')
+      prefs'
+    )}
+
+    ${extraPrefs}
+  '';
+
+  profilesIni = lib.generators.toINI {} (
+    lib.flip lib.mapAttrs' cfg.profiles (
+      _: profile:
+        lib.nameValuePair "Profile${toString profile.id}" {
+          Name = profile.name;
+          Path = profile.path;
+          IsRelative = 1;
+          Default =
+            if profile.isDefault
+            then 1
+            else 0;
+        }
+    )
+    // {
+      General = {
+        StartWithLastProfile = 1;
+      };
+    }
+  );
+
+  effectivePolicies =
+    cfg.policies
+    // lib.optionalAttrs (cfg.languagePacks != []) {
+      ExtensionSettings =
+        (cfg.policies.ExtensionSettings or {})
+        // lib.listToAttrs (map (lang:
+          lib.nameValuePair "langpack-${lang}@firefox.mozilla.org" {
+            installation_mode = "normal_installed";
+            install_url = "https://releases.mozilla.org/pub/firefox/releases/${cfg.release}/linux-x86_64/xpi/${lang}.xpi";
+          })
+        cfg.languagePacks);
+    };
+
+  mkSearchModule = {
+    pkgs,
+    modulePath,
+    profilePath,
+    package,
+  }: {
+    config,
+    lib,
+    ...
+  }: let
+    internalFieldNames =
+      lib.genAttrs
+      [
+        "name"
+        "isAppProvided"
+        "loadPath"
+        "updateInterval"
+        "updateURL"
+        "iconMapObj"
+        "metaData"
+        "orderHint"
+        "definedAliases"
+        "urls"
+      ] (name: "_${name}")
+      // {
+        searchForm = "__searchForm";
+      };
+
+    iconUrl = icon:
+      if lib.isPath icon || lib.hasPrefix "/" icon
+      then "file://${icon}"
+      else icon;
+
+    processCustomEngineInput = input:
+      {
+        name = input.id;
+      }
+      // (removeAttrs input ["icon"])
+      // optionalAttrs (input ? icon || input ? iconMapObj) {
+        iconMapObj = lib.mapAttrs (_name: iconUrl) (
+          (optionalAttrs (input ? icon) {
+            # Convenience to specify single icon instead of a map
+            "16" = input.icon;
+          })
+          // (input.iconMapObj or {})
+        );
+      }
+      // {
+        # Required for custom engine configurations; loadPaths are
+        # unique identifiers generally formatted as [source]/path/to/engine
+        loadPath = "[hjem]/${lib.showAttrPath (modulePath ++ ["engines" input.id])}";
+      };
+
+    processEngineInput = id: input: let
+      requiredInput = {
+        inherit id;
+        isAppProvided = input.isAppProvided or (removeAttrs input ["metaData"] == {});
+        metaData = input.metaData or {};
+      };
+    in
+      if requiredInput.isAppProvided
+      then requiredInput
+      else processCustomEngineInput (input // requiredInput);
+
+    buildEngineConfig = name: input:
+      lib.mapAttrs' (name: value: {
+        name = internalFieldNames.${name} or name;
+        inherit value;
+      }) (processEngineInput name input);
+
+    sortEngineConfigs = configs: let
+      buildEngineConfigWithOrder = order: id: let
+        config =
+          configs.${
+            id
+          } or {
+            inherit id;
+            _isAppProvided = true;
+            _metaData = {};
+          };
+      in
+        config // {_metaData = config._metaData // {inherit order;};};
+
+      engineConfigsWithoutOrder = lib.attrValues (removeAttrs configs config.order);
+
+      sortedEngineConfigs =
+        (lib.imap buildEngineConfigWithOrder config.order) ++ engineConfigsWithoutOrder;
+    in
+      sortedEngineConfigs;
+
+    engineInput =
+      config.engines
+      // {
+        # Infer defaults as app-provided engines if they're not in engines.
+        ${config.default} = config.engines.${config.default} or {};
+      }
+      // {
+        ${config.privateDefault} = config.engines.${config.privateDefault} or {};
+      };
+
+    settings = {
+      version = 12;
+      engines = sortEngineConfigs (lib.mapAttrs buildEngineConfig engineInput);
+
+      metaData =
+        optionalAttrs (config.default != null) {
+          defaultEngineId = config.default;
+          defaultEngineIdHash = "@hash@";
+        }
+        // optionalAttrs (config.privateDefault != null) {
+          privateDefaultEngineId = config.privateDefault;
+          privateDefaultEngineIdHash = "@privateHash@";
+        }
+        // {
+          useSavedOrder = config.order != [];
+        };
+    };
+
+    disclaimer =
+      "By modifying this file, I agree that I am doing so "
+      + "only within @appName@ itself, using official, user-driven search "
+      + "engine selection processes, and in a way which does not circumvent "
+      + "user consent. I acknowledge that any attempt to change this file "
+      + "from outside of @appName@ is a malicious act, and will be responded "
+      + "to accordingly.";
+
+    salt =
+      if config.default != null
+      then profilePath + config.default + disclaimer
+      else null;
+
+    privateSalt =
+      if config.privateDefault != null
+      then profilePath + config.privateDefault + disclaimer
+      else null;
+
+    appNameVariable =
+      if package == null
+      then "appName=${lib.escapeShellArg appName}"
+      else ''
+        applicationIni="$(find ${lib.escapeShellArg package} -maxdepth 3 -path ${lib.escapeShellArg package}'/lib/*/application.ini' -print -quit)"
+        if test -n "$applicationIni"; then
+          appName="$(sed -n 's/^Name=\(.*\)$/\1/p' "$applicationIni" | head -n1)"
+        else
+          appName=${lib.escapeShellArg appName}
+        fi
+      '';
+
+    file =
+      pkgs.runCommand "search.json.mozlz4" {
+        nativeBuildInputs = with pkgs; [
+          mozlz4a
+          openssl
+        ];
+        json = builtins.toJSON settings;
+        inherit salt privateSalt;
+      } ''
+        ${appNameVariable}
+
+        salt=''${salt//@appName@/"$appName"}
+        privateSalt=''${privateSalt//@appName@/"$appName"}
+
+        if [[ -n $salt ]]; then
+          export hash=$(echo -n "$salt" | openssl dgst -sha256 -binary | base64)
+          export privateHash=$(echo -n "$privateSalt" | openssl dgst -sha256 -binary | base64)
+          mozlz4a <(substituteStream json search.json.in --subst-var hash --subst-var privateHash) "$out"
+        else
+          mozlz4a <(echo "$json") "$out"
+        fi
+      '';
+  in {
+    options = {
+      enable = mkOption {
+        type = bool;
+        default = config.default != null || config.privateDefault != null || config.order != [] || config.engines != {};
+        internal = true;
+      };
+
+      force = mkOption {
+        type = bool;
+        default = false;
+        description = ''
+          Whether to force replace the existing search configuration.
+        '';
+      };
+
+      default = mkOption {
+        type = nullOr str;
+        default = null;
+      };
+
+      privateDefault = mkOption {
+        type = nullOr str;
+        default = null;
+      };
+
+      order = mkOption {
+        type = listOf str;
+        default = [];
+      };
+
+      engines = mkOption {
+        type = attrsOf (attrsOf jsonFormat.type);
+        default = {};
+      };
+
+      file = mkOption {
+        type = path;
+        default = file;
+        internal = true;
+        readOnly = true;
+      };
+    };
+  };
+in {
+  options = lib.setAttrByPath modulePath {
+    enable = mkOption {
+      type = bool;
+      default = false;
+      example = true;
+    };
+
+    package = mkOption {
+      type = nullOr package;
+      default = pkgs.floorp-bin;
+      defaultText = lib.literalExpression "pkgs.floorp-bin";
+    };
+
+    finalPackage = mkOption {
+      type = nullOr package;
+      readOnly = true;
+    };
+
+    release = mkOption {
+      internal = true;
+      type = str;
+    };
+
+    policies = mkOption {
+      type = attrsOf jsonFormat.type;
+      default = {};
+    };
+
+    languagePacks = mkOption {
+      type = listOf str;
+      default = [];
+    };
+
+    profiles = mkOption {
+      type = attrsOf (submodule (
+        {
+          name,
+          config,
+          ...
+        }: {
+          options = {
+            name = mkOption {
+              type = str;
+              default = name;
+            };
+
+            id = mkOption {
+              type = ints.unsigned;
+              default = 0;
+            };
+
+            isDefault = mkOption {
+              type = bool;
+              default = config.id == 0;
+            };
+
+            path = mkOption {
+              type = str;
+              default = name;
+            };
+
+            preConfig = mkOption {
+              type = str;
+              default = "";
+            };
+
+            settings = mkOption {
+              type = attrsOf jsonFormat.type;
+              default = {};
+            };
+
+            extraConfig = mkOption {
+              type = str;
+              default = "";
+            };
+
+            search = mkOption {
+              type = submodule (mkSearchModule {
+                inherit pkgs;
+                modulePath = modulePath ++ ["profiles" name "search"];
+                profilePath = config.path;
+                package = cfg.finalPackage;
+              });
+              default = {};
+            };
+
+            extensions = mkOption {
+              type = submodule {
+                options = {
+                  packages = mkOption {
+                    type = listOf package;
+                    default = [];
+                  };
+
+                  force = mkOption {
+                    type = bool;
+                    default = false;
+                  };
+
+                  settings = mkOption {
+                    type = attrsOf (submodule {
+                      options = {
+                        settings = mkOption {
+                          type = attrsOf jsonFormat.type;
+                          default = {};
+                        };
+
+                        force = mkOption {
+                          type = bool;
+                          default = false;
+                        };
+                      };
+                    });
+                    default = {};
+                  };
+                };
+              };
+              default = {};
+            };
+          };
+        }
+      ));
+      default = {};
+    };
+  };
+
+  config = mkIf cfg.enable (
+    {
+      assertions =
+        mapAttrsToList (name: profile: {
+          assertion = !(extensionSettingsMissingForce profile.extensions.settings) || profile.extensions.force;
+          message = ''
+            programs.floorp: profile '${name}': using 'profiles.${name}.extensions.settings' will
+            override all previous extension settings. Enable either
+            'profiles.${name}.extensions.force' or the corresponding
+            'profiles.${name}.extensions.settings.<extensionId>.force' to acknowledge this.
+          '';
+        })
+        cfg.profiles
+        ++ [
+          {
+            assertion = cfg.languagePacks == [] || cfg.package != null;
+            message = "programs.floorp: languagePacks requires package to be set to a non-null value.";
+          }
+        ];
+
+      packages = lib.optional (cfg.finalPackage != null) cfg.finalPackage;
+
+      files = mkMerge ([
+          (mkIf (cfg.profiles != {}) {
+            "${configPath}/profiles.ini".text = profilesIni;
+          })
+        ]
+        ++ mapAttrsToList (
+          _: profile: let
+            extensionPackages =
+              builtins.filter (pkg: pkg ? addonId) profile.extensions.packages;
+            skippedExtensions =
+              builtins.filter (pkg: !(pkg ? addonId)) profile.extensions.packages;
+          in
+            mkMerge [
+              {
+                "${configPath}/${profile.path}".type = "directory";
+              }
+
+              (mkIf (
+                  profile.preConfig
+                  != ""
+                  || profile.settings != {}
+                  || profile.extraConfig != ""
+                  || extensionSettingsNeedForce profile.extensions.settings
+                ) {
+                  "${configPath}/${profile.path}/user.js".text = mkUserJs profile.preConfig profile.settings profile.extraConfig profile.extensions.settings;
+                })
+
+              (mkIf profile.search.enable {
+                "${configPath}/${profile.path}/search.json.mozlz4" = {
+                  source = profile.search.file;
+                  clobber = profile.search.force;
+                };
+              })
+
+              (mkIf (profile.extensions.packages != []) (
+                lib.warnIf (skippedExtensions != [])
+                "programs.floorp: extensions without `addonId` cannot be linked into the profile: ${
+                  builtins.concatStringsSep ", " (map (pkg: pkg.name) skippedExtensions)
+                }"
+                ({
+                    "${configPath}/${profile.path}/extensions" = {
+                      type = "directory";
+                      clobber = true;
+                    };
+                  }
+                  // lib.listToAttrs (map (pkg: {
+                      name = "${configPath}/${profile.path}/extensions/${pkg.addonId}.xpi";
+                      value = {
+                        source = "${pkg}/share/mozilla/${extensionPath}/${pkg.addonId}.xpi";
+                        clobber = true;
+                      };
+                    })
+                    extensionPackages))
+              ))
+
+              (mkMerge (mapAttrsToList
+                (
+                  extId: ext:
+                    mkIf (ext.settings != {}) {
+                      "${configPath}/${profile.path}/browser-extension-data/${extId}/storage.js" = {
+                        text = lib.generators.toJSON {} ext.settings;
+                        clobber = ext.force || profile.extensions.force;
+                      };
+                    }
+                )
+                profile.extensions.settings))
+            ]
+        )
+        cfg.profiles);
+    }
+    // lib.setAttrByPath modulePath {
+      finalPackage =
+        if cfg.package == null
+        then null
+        else if cfg.package.override.__functionArgs ? cfg
+        then
+          cfg.package.override (old: {
+            cfg = old.cfg or {};
+            extraPolicies = (old.extraPolicies or {}) // effectivePolicies;
+          })
+        else
+          lib.warn
+          "programs.floorp: package does not support overriding; policies and language packs will not be applied."
+          cfg.package;
+
+      release = lib.mkOptionDefault (builtins.head (lib.splitString "-" cfg.package.version));
+    }
+  );
+}
modules/desktop/app/browser/floorp/default.nix
@@ -0,0 +1,219 @@
+{
+  den,
+  lib,
+  ...
+}: {
+  den.aspects.desktop.app.browser.floorp = {
+    includes = [den.aspects.desktop.app.browser];
+
+    persistHome = {config, ...}: {
+      directories = [
+        "${config.directory}/.floorp"
+      ];
+    };
+
+    hjem = {pkgs, ...}: {
+      imports = [./_hjem-module.nix];
+
+      programs.floorp = {
+        enable = true;
+
+        policies = {
+          DisableAppUpdate = true;
+          DisablePocket = true;
+          DisableSetDesktopBackground = true;
+          DisableTelemetry = true;
+          DontCheckDefaultBrowser = true;
+          ExtensionUpdate = false;
+          NoDefaultBookmarks = true;
+          OfferToSaveLogins = false;
+          PasswordManagerEnabled = false;
+        };
+
+        profiles.default = {
+          id = 0;
+          isDefault = true;
+
+          search = {
+            force = true;
+            default = "kagi";
+            privateDefault = "kagi";
+            order = [
+              "kagi"
+              "bing"
+              "google"
+              "ddg"
+            ];
+            engines = {
+              kagi = {
+                name = "Kagi";
+                description = "A privacy-focused, user-centric search engine.";
+                urls = [
+                  {template = "https://kagi.com/search?q={searchTerms}";}
+                  {
+                    type = "application/x-suggestions+json";
+                    template = "https://kagisuggest.com/api/autosuggest?q={searchTerms}";
+                  }
+                ];
+                iconMapObj = {
+                  "16" = "https://kagi.com/favicon-16x16.png";
+                  "32" = "https://kagi.com/favicon-32x32.png";
+                };
+                definedAliases = ["@kagi"];
+              };
+
+              baidu.metaData.hidden = true;
+              startpage.metaData.hidden = true;
+              "you.com".metaData.hidden = true;
+            };
+          };
+
+          extensions.force = true;
+          extensions.packages = with pkgs.nur.repos.rycee.firefox-addons; [
+            ublock-origin
+            bitwarden
+            tampermonkey
+            kiss-translator
+          ];
+
+          settings = {
+            # == General ==
+            "browser.startup.page" = 3; # Open prev session pages
+            "browser.tabs.unloadOnLowMemory" = true;
+            "widget.use-xdg-desktop-portal.file-picker" = 1;
+            # No first run
+            "app.normandy.first_run" = false;
+            "doh-rollout.doneFirstRun" = true;
+            "toolkit.telemetry.reportingpolicy.firstRun" = false;
+            "trailhead.firstrun.didSeeAboutWelcome" = true;
+            "browser.toolbarbuttons.introduced.sidebar-button" = true;
+            "sidebar.old-sidebar.has-used" = true;
+            "sidebar.new-sidebar.has-used" = true;
+            "floorp.browser.welcome.page.shown" = true;
+            # Language
+            "general.useragent.locale" = "zh-CN";
+            "intl.locale.requested" = "zh-CN,en-US";
+            "browser.translations.mostRecentTargetLanguages" = "zh-Hans";
+            # No ads
+            "browser.urlbar.suggest.topsites" = false;
+            "browser.urlbar.suggest.trending" = false;
+            "browser.newtabpage.pinned" = [];
+            "browser.newtabpage.activity-stream.showSponsored" = false;
+            "browser.newtabpage.activity-stream.showSponsoredTopSites" = false;
+            "browser.ai.control.sidebarChatbot" = "blocked";
+            # == Telemetry ==
+            "toolkit.telemetry.unified" = false;
+            "toolkit.telemetry.enabled" = false;
+            "toolkit.telemetry.server" = "data:,";
+            "toolkit.telemetry.archive.enabled" = false;
+            "toolkit.telemetry.newProfilePing.enabled" = false;
+            "toolkit.telemetry.shutdownPingSender.enabled" = false;
+            "toolkit.telemetry.updatePing.enabled" = false;
+            "toolkit.telemetry.bhrPing.enabled" = false;
+            "toolkit.telemetry.firstShutdownPing.enabled" = false;
+            "toolkit.telemetry.shutdownPingSender.enabledFirstsession" = false;
+            "browser.ping-centre.telemetry" = false;
+            "browser.newtabpage.activity-stream.feeds.telemetry" = false;
+            "browser.newtabpage.activity-stream.telemetry" = false;
+            # == Appearance ==
+            "browser.newtabpage.activity-stream.feeds.topsites" = false;
+            "browser.newtabpage.activity-stream.default.sites" = "";
+            "browser.toolbars.bookmarks.visibility" = "always";
+            "sidebar.verticalTabs" = true;
+            "sidebar.main.tools" = "syncedtabs";
+            "sidebar.visibility" = "expand-on-hover";
+            # Floorp's new tab page
+            "floorp.newtab.configs" = {
+              "backround"."type" = "none";
+              "components" = {
+                "topSites" = false;
+                "clock" = false;
+                "searchBar" = true;
+                "firefoxLayout" = true;
+              };
+              "searchBar" = {"searchEngine" = "default";};
+              "topSites" = {
+                "pinned" = [];
+                "blocked" = [];
+              };
+            };
+            # panelSideBar
+            "floorp.panelSidebar.enabled" = true;
+            "floorp.panelSidebar.config" = {
+              "autoUnload" = false;
+              "position_start" = true;
+              "globalWidth" = 400;
+              "displayed" = true;
+              "webExtensionRunningEnabled" = false;
+            };
+            "floorp.panelSidebar.data"."data" =
+              (lib.map (x: {
+                "id" = "default-panel-${x}";
+                "url" = "floorp//${x}";
+                "width" = 0;
+                "type" = "static";
+              }) ["bookmarks" "history" "downloads"])
+              ++ (lib.map (url: let
+                  id = lib.pipe url [
+                    (x: lib.removePrefix "http://" x)
+                    (x: lib.removePrefix "https://" x)
+                    (x: lib.splitString "?" x)
+                    (x: lib.elemAt x 0)
+                    (x: lib.replaceStrings ["/" "."] ["-" "-"] x)
+                    (x: lib.removeSuffix "-" x)
+                    (x: "panel-${x}")
+                  ];
+                in {
+                  inherit id url;
+                  "width" = 0;
+                  "userContextId" = 0;
+                  "zoomLevel" = null;
+                  "type" = "web";
+                }) [
+                  "https://translate.kagi.com"
+                  "https://squoosh.app"
+                ]);
+            # Workspace
+            "floorp.workspaces.enabled" = true;
+            "floorp.workspaces.v4.config" = {
+              "manageOnBms" = true;
+              "showWorkspaceNameOnToolbar" = true;
+            };
+            # Top toolbar
+            "browser.uiCustomization.state" = {
+              "placements" = {
+                "widget-overflow-fixed-list" = [];
+                "unified-extensions-area" = [
+                  "firefox_tampermonkey_net-browser-action" # Tampermonkey
+                  "ublock0_raymondhill_net-browser-action" # uBlock Origin
+                ];
+                "nav-bar" = [
+                  "sidebar-button"
+                  "back-button"
+                  "forward-button"
+                  "stop-reload-button"
+                  "vertical-spacer"
+                  "urlbar-container"
+                  "_446900e4-71c2-419f-a6a7-df9c091e268b_-browser-action" # Bitwarden
+                  "unified-extensions-button"
+                  "fxa-toolbar-menu-button"
+                ];
+                "toolbar-menubar" = ["menubar-items"];
+                "TabsToolbar" = [];
+                "vertical-tabs" = ["tabbrowser-tabs"];
+                "PersonalToolbar" = ["personal-bookmarks"];
+                "nora-statusbar" = [
+                  "screenshot-button"
+                  "fullscreen-button"
+                  "status-text"
+                ];
+              };
+              "seen" = ["developer-button"];
+              "dirtyAreaCache" = [];
+            };
+          };
+        };
+      };
+    };
+  };
+}
modules/desktop/app/browser/chromium.nix
@@ -0,0 +1,34 @@
+{
+  den,
+  lib,
+  ...
+}: {
+  den.aspects.desktop.app.browser.chromium = {
+    includes = [den.aspects.desktop.app.browser];
+
+    persistHome = {config, ...}: {
+      directories = [
+        "${config.xdg.config.directory}/chromium"
+      ];
+    };
+
+    hjem = {pkgs, ...}: let
+      extensions = [
+        "nngceckbapebfimnlniiiahkandclblb" # Bitwarden
+        "ddkjiahejlhfcafbddmgiahcphecmpfh" # Ublock Origin Lite
+      ];
+    in {
+      packages = [pkgs.chromium];
+
+      xdg.config.files =
+        lib.genAttrs'
+        extensions
+        (id: {
+          name = "chromium/External Extensions/${id}.json";
+          value.text = builtins.toJSON {
+            external_update_url = "https://clients2.google.com/service/update2/crx";
+          };
+        });
+    };
+  };
+}
modules/desktop/app/browser/default.nix
@@ -0,0 +1,36 @@
+{lib, ...}: {
+  den.aspects.desktop.app.browser = {
+    settings.user = {
+      defaults = lib.mkOption {
+        type = lib.types.listOf lib.types.str;
+        description = "Default browsers' desktop entries";
+        default = [];
+      };
+    };
+    hjem = {user, ...}: let
+      cfg = user.system.settings.desktop.app.browser;
+      desktopEntries = cfg.defaults;
+    in {
+      xdg.mime-apps.default-applications = {
+        "application/json" = desktopEntries;
+        "text/html" = desktopEntries;
+        "text/xml" = desktopEntries;
+        "application/xml" = desktopEntries;
+        "application/xhtml+xml" = desktopEntries;
+        "application/xhtml_xml" = desktopEntries;
+        "application/rdf+xml" = desktopEntries;
+        "application/rss+xml" = desktopEntries;
+        "application/x-extension-htm" = desktopEntries;
+        "application/x-extension-html" = desktopEntries;
+        "application/x-extension-shtml" = desktopEntries;
+        "application/x-extension-xht" = desktopEntries;
+        "application/x-extension-xhtml" = desktopEntries;
+
+        "x-scheme-handler/about" = desktopEntries;
+        "x-scheme-handler/ftp" = desktopEntries;
+        "x-scheme-handler/http" = desktopEntries;
+        "x-scheme-handler/https" = desktopEntries;
+      };
+    };
+  };
+}
modules/desktop/app/term/default.nix
@@ -0,0 +1,19 @@
+{lib, ...}: {
+  den.aspects.desktop.app.term = {
+    settings.user = {
+      default = lib.mkOption {
+        type = lib.types.nullOr lib.types.str;
+        description = "Default terminal's binary name";
+        default = null;
+      };
+    };
+
+    provides.to-users = {
+      hjem = {pkgs, ...}: {
+        packages = [pkgs.xdg-terminal-exec];
+
+        xdg.config.files."xdg-terminals.list".text = "";
+      };
+    };
+  };
+}
modules/desktop/app/term/foot.nix
@@ -0,0 +1,34 @@
+{den, ...}: {
+  den.aspects.desktop.app.term.foot = {
+    includes = [den.aspects.desktop.app.term];
+    hjem = {
+      user,
+      pkgs,
+      lib,
+      ...
+    }: {
+      packages = [pkgs.foot];
+
+      xdg.config.files."foot/foot.ini" = {
+        generator = (pkgs.formats.ini {}).generate "foot-config.ini";
+        value = {
+          main = {
+            pad = "10x10";
+          };
+        };
+      };
+
+      xdg.config.files."xdg-terminals.list".text =
+        lib.mkOrder
+        (1000 # Default order
+          + (
+            if (user.system.settings.desktop.app.term.default == "foot")
+            then -500 # lib.mkBefore
+            else 500 # lib.mkAfter
+          ))
+        ''
+          foot.desktop
+        '';
+    };
+  };
+}
modules/desktop/app/imv.nix
@@ -0,0 +1,17 @@
+{
+  den.aspects.desktop.app.imv = {
+    hjem = {pkgs, ...}: {
+      packages = [pkgs.imv];
+
+      xdg.mime-apps.default-applications = {
+        "image/jpeg" = ["imv.desktop"];
+        "image/avif" = ["imv.desktop"];
+        "image/png" = ["imv.desktop"];
+        "image/gif" = ["imv.desktop"];
+        "image/webp" = ["imv.desktop"];
+        "image/svg+xml" = ["imv.desktop"];
+        "image/bmp" = ["imv.desktop"];
+      };
+    };
+  };
+}
modules/desktop/app/kdeconnect.nix
@@ -0,0 +1,39 @@
+{
+  den.aspects.desktop.app.kdeconnect = {
+    persistHome = {config, ...}: {
+      directories = [
+        "${config.xdg.config.directory}/kdeconnect"
+      ];
+    };
+
+    nixos = {
+      networking.firewall = {
+        allowedTCPPortRanges = [
+          {
+            from = 1714;
+            to = 1764;
+          }
+        ];
+        allowedUDPPortRanges = [
+          {
+            from = 1714;
+            to = 1764;
+          }
+        ];
+      };
+    };
+
+    hjem = {pkgs, ...}: {
+      packages = [pkgs.kdePackages.kdeconnect-kde];
+
+      systemd.services.kdeconnect = {
+        description = "Adds communication between your desktop and your smartphone";
+        partOf = ["graphical-session.target"];
+        after = ["graphical-session.target"];
+        wantedBy = ["graphical-session.target"];
+        script = "${pkgs.kdePackages.kdeconnect-kde}/bin/kdeconnectd";
+        serviceConfig.Restart = "on-abort";
+      };
+    };
+  };
+}
modules/desktop/app/kdenlive.nix
@@ -0,0 +1,16 @@
+{
+  den.aspects.desktop.app.kdenlive = {
+    persistHome = {config, ...}: {
+      directories = [
+        "${config.xdg.data.directory}/kdenlive"
+      ];
+      files = [
+        "${config.xdg.config.directory}/kdenliverc"
+      ];
+    };
+
+    hjem = {pkgs, ...}: {
+      packages = [pkgs.kdePackages.kdenlive];
+    };
+  };
+}
modules/desktop/app/libreoffice.nix
@@ -0,0 +1,17 @@
+{
+  den.aspects.desktop.app.libreoffice = {
+    persistHome = {config, ...}: {
+      directories = [
+        "${config.xdg.config.directory}/libreoffice"
+      ];
+    };
+
+    nixos = {pkgs, ...}: {
+      fonts.packages = [pkgs.nur.repos.rewine.ttf-ms-win10];
+    };
+
+    hjem = {pkgs, ...}: {
+      packages = [pkgs.libreoffice];
+    };
+  };
+}
modules/desktop/app/mpv.nix
@@ -0,0 +1,148 @@
+{
+  den.aspects.desktop.app.mpv = {
+    cacheHome = {config, ...}: {
+      directories = [
+        "${config.xdg.state.directory}/mpv"
+      ];
+    };
+
+    hjem = {
+      pkgs,
+      lib,
+      ...
+    }: let
+      # Port of home-manager's programs.mpv
+      # See https://github.com/nix-community/home-manager/blob/c53d643b3737e2fcd04e6cb3b3580ef50b2087a0/modules/programs/mpv.nix
+      mpvConfGenerator = {
+        defaultProfiles ? [],
+        config ? {},
+        profiles ? {},
+        includes ? [],
+      }: let
+        renderOption = option:
+          if lib.isBool option
+          then
+            (
+              if option
+              then "yes"
+              else "no"
+            )
+          else toString option;
+
+        renderOptionValue = value: let
+          rendered = renderOption value;
+        in "%${toString (builtins.stringLength rendered)}%${rendered}";
+
+        mkKeyValue = lib.generators.mkKeyValueDefault {
+          mkValueString = renderOptionValue;
+        } "=";
+
+        renderOptions = lib.generators.toKeyValue {
+          inherit mkKeyValue;
+          listsAsDuplicateKeys = true;
+        };
+
+        renderProfiles = lib.generators.toINI {
+          inherit mkKeyValue;
+          listsAsDuplicateKeys = true;
+        };
+
+        renderDefaultProfiles = profiles: renderOptions {profile = lib.concatStringsSep "," profiles;};
+      in
+        lib.concatStringsSep "\n" (lib.filter (s: s != "") [
+          (lib.optionalString (defaultProfiles != []) (renderDefaultProfiles defaultProfiles))
+          (lib.optionalString (config != {}) (renderOptions config))
+          (lib.optionalString (profiles != {}) (renderProfiles profiles))
+          (lib.concatMapStringsSep "\n" (include: "include=${include}") includes)
+        ]);
+
+      mpvScriptOptsGenerator = value:
+        lib.generators.toKeyValue {
+          mkKeyValue = lib.generators.mkKeyValueDefault {
+            mkValueString = option:
+              if lib.isBool option
+              then
+                (
+                  if option
+                  then "yes"
+                  else "no"
+                )
+              else toString option;
+          } "=";
+          listsAsDuplicateKeys = true;
+        }
+        value;
+    in {
+      packages = [
+        (pkgs.mpv.override {
+          scripts = with pkgs.mpvScripts; [
+            mpris
+            uosc
+            thumbfast
+            autoload
+            reload
+            mpv-playlistmanager
+          ];
+        })
+      ];
+
+      xdg.config.files = {
+        "mpv/mpv.conf" = {
+          generator = mpvConfGenerator;
+          value = {
+            defaultProfiles = ["gpu-hq"];
+            config = {
+              vo = "gpu-next";
+              hwdec = "auto-copy";
+              scale = "ewa_lanczossharp";
+              # --- ๅŠจๆ€่Œƒๅ›ดไธŽ่‰ฒๅฝฉ็ฎก็† --- #
+              target-colorspace-hint = "auto";
+              tone-mapping = "hable";
+              dither = "fruit";
+              dither-depth = "auto";
+              # --- ้Ÿณ้ข‘่ดจ้‡้…็ฝฎ --- #
+              ao = "pipewire";
+              audio-resample-filter-size = 64;
+              audio-resample-phase-shift = 10;
+              # --- ๅญ—ๅน•้…็ฝฎ --- #
+              sub-auto = "fuzzy";
+              sub-bold = "yes";
+              sub-outline-size = 2.25;
+              sub-outline-color = "#111111";
+              sub-color = "#FEFEFE";
+              sub-font-size = "36";
+              sub-use-margins = "yes";
+              sub-ass-override = "force";
+              # --- ็”จๆˆทไฝ“้ชŒ --- #
+              save-position-on-quit = true;
+              keep-open = "yes";
+              osd-bar = "no"; # use uosc
+              # ้Ÿณ้‡ๆŽงๅˆถ
+              volume = 80;
+              volume-max = 120;
+              # OSD ๆ˜พ็คบ
+              osd-duration = 2500;
+              osd-font-size = 32;
+              # ๆˆชๅ›พ่ฎพ็ฝฎ
+              screenshot-format = "png";
+              screenshot-dir = "$XDG_PICTURES_DIR/mpv";
+              screenshot-template = "%F-%P";
+            };
+          };
+        };
+
+        "mpv/script-opts/uosc.conf" = {
+          generator = mpvScriptOptsGenerator;
+          value = {
+            languages = "slang,zh-hans";
+          };
+        };
+      };
+
+      xdg.mime-apps.default-applications = {
+        "audio/*" = ["mpv.desktop"];
+        "video/*" = ["mpv.desktop"];
+      };
+    };
+  };
+}
modules/desktop/app/obs-studio.nix
@@ -0,0 +1,34 @@
+{
+  den.aspects.desktop.app.obs-studio = {
+    persistHome = {config, ...}: {
+      directories = [
+        "${config.xdg.config.directory}/obs-studio"
+      ];
+    };
+
+    nixos = {config, ...}: {
+      # From https://github.com/NixOS/nixpkgs/blob/f8cc9d1b80b6c75fec21ecc549f631c3eed5a5e1/nixos/modules/programs/obs-studio.nix
+      boot = {
+        kernelModules = ["v4l2loopback"];
+        extraModulePackages = [config.boot.kernelPackages.v4l2loopback];
+
+        extraModprobeConfig = ''
+          options v4l2loopback devices=1 video_nr=1 card_label="OBS Cam" exclusive_caps=1
+        '';
+      };
+    };
+
+    hjem = {pkgs, ...}: {
+      packages = [
+        (pkgs.wrapOBS {
+          plugins = with pkgs.obs-studio-plugins; [
+            input-overlay
+            obs-pipewire-audio-capture
+            obs-vkcapture
+            obs-backgroundremoval
+          ];
+        })
+      ];
+    };
+  };
+}
modules/desktop/app/picard.nix
@@ -0,0 +1,15 @@
+{
+  den.aspects.desktop.app.picard = {
+    persistHome = {config, ...}: {
+      directories = [
+        "${config.xdg.config.directory}/MusicBrainz"
+      ];
+    };
+
+    hjem = {pkgs, ...}: {
+      packages = with pkgs; [
+        picard
+      ];
+    };
+  };
+}
modules/desktop/game/alsa-ucm-dualsense-haptics.patch
@@ -0,0 +1,22 @@
+diff --git a/ucm2/USB-Audio/Sony/DualSense-PS5.conf b/ucm2/USB-Audio/Sony/DualSense-PS5.conf
+index d6bfb4a..ed7157a 100644
+--- a/ucm2/USB-Audio/Sony/DualSense-PS5.conf
++++ b/ucm2/USB-Audio/Sony/DualSense-PS5.conf
+@@ -5,6 +5,17 @@ Include.dhw.File "/common/directm.conf"
+ # keep this use case first - wine compatibility
+ Macro.0.DirectUseCase { Id="Direct" PlaybackChannels=4 CaptureChannels=2 }
+
++If.setvol.Prepend.SectionUseCase."Direct".Config {
++       SectionVerb {
++               EnableSequence [
++                       cset "name='PCM Playback Volume' 100"
++               ]
++       }
++       SectionDevice."Direct".Value {
++               PlaybackMixerElem "PCM"
++       }
++}
++
+ If.default.Prepend.SectionUseCase."Default" {
+        Comment "Default"
+        File "/USB-Audio/Sony/DualSense-PS5-HiFi.conf"
modules/desktop/game/gamepad.nix
@@ -0,0 +1,32 @@
+{den, ...}: {
+  den.aspects.desktop.game.includes = [den.aspects.desktop.game.gamepad];
+  den.aspects.desktop.game.gamepad = {
+    nixos = {
+      pkgs,
+      config,
+      ...
+    }: {
+      hardware = {
+        uinput.enable = true;
+        xone.enable = true;
+        xpadneo.enable = true;
+      };
+
+      boot = {
+        extraModulePackages = [
+          config.boot.kernelPackages.xpadneo
+        ];
+        extraModprobeConfig = ''
+          options bluetooth disable_ertm=Y
+        '';
+        kernelModules = [
+          "hid_microsoft"
+        ];
+      };
+
+      services.udev.packages = [
+        pkgs.game-devices-udev-rules
+      ];
+    };
+  };
+}
modules/desktop/game/heroic.nix
@@ -0,0 +1,27 @@
+{lib, ...}: {
+  den.aspects.desktop.game.heroic = {
+    cacheHome = {config, ...}: {
+      directories = [
+        "${config.xdg.config.directory}/heroic"
+        "${config.xdg.state.directory}/Heroic"
+        "${config.xdg.data.directory}/umu"
+      ];
+    };
+
+    hjem = {
+      pkgs,
+      osConfig,
+      ...
+    }: {
+      packages = [
+        pkgs.heroic
+      ];
+
+      xdg.config.files = lib.mergeAttrsList (
+        map
+        (w: {"heroic/tools/proton/${w.pname}".source = w.steamcompattool;})
+        osConfig.programs.steam.extraCompatPackages
+      );
+    };
+  };
+}
modules/desktop/game/hoyo.nix
@@ -0,0 +1,38 @@
+{
+  den.aspects.desktop.game.hoyo = {
+    nixos = {
+      networking.hosts = {
+        "0.0.0.0" = [
+          "osuspider.yuanshen.com"
+          "overseauspider.yuanshen.com"
+          "uspider.yuanshen.com"
+
+          "log-upload-os.hoyoverse.com"
+          "log-upload-os.mihoyo.com"
+          "apm-log-upload-os.hoyoverse.com"
+          # "zzz-log-upload-os.hoyoverse.com"
+          "log-upload.mihoyo.com"
+          "ys-log-upload.mihoyo.com"
+          "ys-log-upload-os.hoyoverse.com"
+          "hkrpg-log-upload-os.hoyoverse.com"
+          "dump.gamesafe.qq.com"
+          "devlog-upload.mihoyo.com"
+
+          "globaldp-prod-cn01.bhsr.com"
+          # "globaldp-prod-cn01.juequling.com"
+
+          "sg-public-data-api.hoyoverse.com"
+          "public-data-api.mihoyo.com"
+
+          "prd-lender.cdp.internal.unity3d.com"
+          "thind-prd-knob.data.ie.unity3d.com"
+          "thind-gke-usc.prd.data.corp.unity3d.com"
+          "cdp.cloud.unity3d.com"
+          "remote-config-proxy-prd.uca.cloud.unity3d.com"
+
+          "pc.crashsight.wetest.net"
+        ];
+      };
+    };
+  };
+}
modules/desktop/game/ludusavi.nix
@@ -0,0 +1,17 @@
+{
+  den.aspects.desktop.game.ludusavi = {
+    persistHome = {config, ...}: {
+      directories = [
+        "${config.xdg.config.directory}/ludusavi"
+      ];
+    };
+
+    hjem = {pkgs, ...}: {
+      # Use ludusavi to avoid add every Linux native game's
+      # data folder into persistence.
+      packages = [
+        pkgs.ludusavi
+      ];
+    };
+  };
+}
modules/desktop/game/mangohud.nix
@@ -0,0 +1,54 @@
+{
+  den.aspects.desktop.game.mangohud = {
+    hjem = {
+      pkgs,
+      lib,
+      ...
+    }: let
+      renderOption = option:
+        rec {
+          int = toString option;
+          float = int;
+          path = int;
+          bool = "0"; # "on/off" opts are disabled with `=0`
+          string = option;
+          list = lib.concatStringsSep "," (lib.lists.forEach option toString);
+        }.${
+          builtins.typeOf option
+        };
+
+      renderLine = k: v: (
+        if lib.isBool v && v
+        then k
+        else "${k}=${renderOption v}"
+      );
+      renderSettings = attrs: lib.strings.concatStringsSep "\n" (lib.attrsets.mapAttrsToList renderLine attrs) + "\n";
+    in {
+      packages = [pkgs.mangohud];
+
+      xdg.config.files."MangoHud/MangoHud.conf" = {
+        generator = renderSettings;
+        value = {
+          fps = true;
+          frametime = true;
+
+          # Hardware stats
+          cpu_stats = true;
+          cpu_temp = true;
+          cpu_power = true;
+          cpu_mhz = true;
+          gpu_stats = true;
+          gpu_temp = true;
+          gpu_power = true;
+          gpu_core_clock = true;
+          gpu_mem_clock = true;
+          ram = true;
+          vram = true;
+
+          # Keybinds
+          toggle_hud = "Shift_R+F12";
+        };
+      };
+    };
+  };
+}
modules/desktop/game/minecraft.nix
@@ -0,0 +1,17 @@
+{
+  den.aspects.desktop.game.minecraft = {
+    cacgeHome = {config, ...}: {
+      directories = [
+        "${config.xdg.data.directory}/PrismLauncher"
+      ];
+    };
+
+    hjem = {pkgs, ...}: {
+      packages = [
+        (pkgs.prismlauncher.override {
+          additionalPrograms = [pkgs.ffmpeg-headless];
+        })
+      ];
+    };
+  };
+}
modules/desktop/game/misc.nix
@@ -0,0 +1,46 @@
+{den, ...}: {
+  den.aspects.desktop.game.includes = [den.aspects.desktop.game.misc];
+  den.aspects.desktop.game.misc = {
+    nixos = {pkgs, ...}: let
+      # Fix DualSense haptics until https://github.com/alsa-project/alsa-ucm-conf/issues/677 been resolved.
+      alsa-ucm-conf-dualsense-haptics = with pkgs;
+        alsa-ucm-conf.overrideAttrs {
+          # https://github.com/alsa-project/alsa-ucm-conf/issues/677#issuecomment-3759801501
+          patches = [
+            ./alsa-ucm-dualsense-haptics.patch
+          ];
+        };
+    in {
+      environment.sessionVariables.ALSA_CONFIG_UCM2 = "${alsa-ucm-conf-dualsense-haptics}/share/alsa/ucm2";
+
+      programs.gamemode.enable = true;
+
+      # load ntsync
+      boot.kernelModules = ["ntsync"];
+      # make ntsync device accessible
+      services.udev.packages = [
+        (pkgs.writeTextFile {
+          name = "ntsync-udev-rules";
+          text = ''KERNEL=="ntsync", MODE="0660", TAG+="uaccess"'';
+          destination = "/etc/udev/rules.d/70-ntsync.rules";
+        })
+      ];
+    };
+
+    provides.to-users = {
+      includes = [
+        {
+          cacheHome = {config, ...}: {
+            directories = [
+              config.xdg.user-dirs.directories.games
+            ];
+          };
+        }
+      ];
+
+      hjem = {config, ...}: {
+        xdg.user-dirs.directories.games = "${config.directory}/Games";
+      };
+    };
+  };
+}
modules/desktop/game/steam.nix
@@ -0,0 +1,66 @@
+{
+  den.aspects.desktop.game.steam = {
+    cacheHome = {config, ...}: {
+      directories = [
+        "${config.xdg.config.directory}/millennium"
+        "${config.xdg.data.directory}/millennium"
+        "${config.xdg.data.directory}/Steam"
+        ".steam"
+      ];
+    };
+
+    nixos = {pkgs, ...}: let
+      millennium-src = pkgs.fetchFromGitHub {
+        owner = "SteamClientHomebrew";
+        repo = "Millennium";
+        rev = "v3.3.1";
+        hash = "sha256-nqNRtEkYgXawzpw/xCAgMSyaHKl5GFvzKm8WOoaUN24=";
+      };
+      millennium = pkgs.callPackage "${millennium-src}/packages/nix/millennium.nix" {inherit millennium-src;};
+      millennium-steam = pkgs.callPackage "${millennium-src}/packages/nix/steam.nix" {inherit millennium;};
+    in {
+      hardware = {
+        steam-hardware.enable = true;
+      };
+
+      programs.steam = {
+        enable = true;
+        dedicatedServer.openFirewall = true;
+
+        extraCompatPackages = [
+          pkgs.proton-ge-bin
+          pkgs.dwproton-bin
+          pkgs.nur.repos.vladexa.proton-cachyos-v3
+        ];
+
+        package = millennium-steam.override {
+          extraEnv = {
+            MANGOHUD = true;
+            OBS_VKCAPTURE = true;
+            PROTON_ENABLE_WAYLAND = true;
+            PROTON_ENABLE_HDR = true;
+            PROTON_USE_NTSYNC = true;
+            PROTON_USE_WOW64 = true;
+          };
+        };
+      };
+
+      # https://steamdeck-packages.steamos.cloud/archlinux-mirror/jupiter-main/os/x86_64/steamos-customizations-jupiter-20250117.1-1-any.pkg.tar.zst
+      boot.kernel.sysctl = {
+        # 20-shed.conf
+        "kernel.sched_cfs_bandwidth_slice_us" = 3000;
+        # 20-net-timeout.conf
+        # This is required due to some games being unable to reuse their TCP ports
+        # if they're killed and restarted quickly - the default timeout is too large.
+        "net.ipv4.tcp_fin_timeout" = 5;
+        # 30-splitlock.conf
+        # Prevents intentional slowdowns in case games experience split locks
+        # This is valid for kernels v6.0+
+        "kernel.split_lock_mitigate" = 0;
+        # 30-vm.conf
+        # USE MAX_INT - MAPCOUNT_ELF_CORE_MARGIN.
+        "vm.max_map_count" = 2147483642;
+      };
+    };
+  };
+}
modules/desktop/ime/fcitx5.nix
@@ -0,0 +1,121 @@
+{
+  den.aspects.desktop.ime.fcitx5 = {
+    persistHome = {config, ...}: {
+      directories = [
+        "${config.xdg.data.directory}/fcitx5/rime" # RIME data
+      ];
+    };
+
+    nixos = {
+      pkgs,
+      config,
+      ...
+    }: let
+      rime-frost = pkgs.stdenvNoCC.mkDerivation (finalAttrs: {
+        pname = "rime-ice";
+        version = "0-unstable-2026-08-16";
+
+        src = pkgs.fetchFromGitHub {
+          owner = "gaboolic";
+          repo = "rime-frost";
+          rev = "6b74e54da2737d9b9ba8c502ce697145f94504ba";
+          hash = "sha256-NKqjw7izkbNll9dM9qhjgW0IXUFz4o3l9uVOckyLx+w=";
+        };
+
+        installPhase = ''
+          runHook preInstall
+
+          rm -rf others README.md .git*
+
+          mv default.yaml rime_frost_suggestion.yaml
+
+          mkdir -p $out/share
+          cp -r . $out/share/rime-data
+
+          runHook postInstall
+        '';
+
+        meta = {
+          homepage = "https://github.com/gaboolic/rime-frost";
+          changelog = "https://github.com/gaboolic/rime-frost/blob/main/others/CHANGELOG.md";
+          license = pkgs.lib.licenses.gpl3Only;
+        };
+      });
+    in {
+      i18n.inputMethod = {
+        enable = true;
+        type = "fcitx5";
+
+        fcitx5 = {
+          addons = with pkgs; [
+            (fcitx5-rime.override {rimeDataPkgs = [rime-frost];})
+            fcitx5-gtk # gtk im module
+          ];
+          waylandFrontend = !config.services.xserver.enable;
+        };
+      };
+    };
+
+    provides.to-users = {
+      hjem = {
+        pkgs,
+        lib,
+        ...
+      }: let
+        iniFormat = pkgs.formats.ini {};
+        iniGlobalFormat = pkgs.formats.iniWithGlobalSection {};
+        yamlFormat = pkgs.formats.yaml {};
+
+        normalizeFcitx5Value = value:
+          if lib.isAttrs value
+          then lib.mapAttrs (_: normalizeFcitx5Value) value
+          else if builtins.isList value
+          then map normalizeFcitx5Value value
+          else if builtins.isBool value
+          then
+            if value
+            then "True"
+            else "False"
+          else value;
+      in {
+        xdg.config.files."fcitx5/profile" = {
+          generator = value: iniFormat.generate "fcitx5-profile" (normalizeFcitx5Value value);
+          value = {
+            GroupOrder."0" = "Default";
+            "Groups/0" = {
+              Name = "Default";
+              "Default Layout" = "us";
+              DefaultIM = "rime";
+            };
+            "Groups/0/Items/0" = {
+              Name = "keyboard-us";
+              Layout = "us";
+            };
+            "Groups/0/Items/1" = {
+              Name = "rime";
+              Layout = "us";
+            };
+          };
+        };
+        xdg.config.files."fcitx5/conf/rime.conf" = {
+          generator = value: iniGlobalFormat.generate "fcitx5-conf-rime.conf" (normalizeFcitx5Value value);
+          value = {
+            globalSection = {
+              PreeditMode = "Composing text";
+              InputState = "All";
+              PreeditCursorPositionAtBeginning = true;
+              SwitchInputMethodBehavior = "Commit commit preview";
+            };
+          };
+        };
+
+        xdg.data.files."fcitx5/rime/default.custom.yaml" = {
+          generator = yamlFormat.generate "fcitx5-rime-default.custom.conf";
+          value = {
+            patch.__include = "rime_frost_suggestion:/";
+          };
+        };
+      };
+    };
+  };
+}
modules/desktop/noctalia/default.nix
@@ -0,0 +1,110 @@
+{
+  den.aspects.desktop.noctalia = {
+    persistHome = {config, ...}: {
+      directories = [
+        "${config.xdg.state.directory}/noctalia"
+      ];
+    };
+
+    hjem = {
+      user,
+      pkgs,
+      lib,
+      ...
+    }: let
+      noctalia-src = pkgs.fetchFromGitHub {
+        owner = "noctalia-dev";
+        repo = "noctalia";
+        tag = "v5.0.0-beta.8";
+        hash = "sha256-qy3Cheg/FQ9ZaBPTIgdq4IkmkNtC6XBpmtC8nT+wU/Y=";
+      };
+      noctalia = pkgs.callPackage "${noctalia-src}/nix/package.nix" {};
+    in {
+      packages = [noctalia];
+
+      xdg.config.files."noctalia/config.toml" = {
+        generator = value: let
+          rawConfig = (pkgs.formats.toml {}).generate "noctalia-config.toml" value;
+        in
+          pkgs.runCommand "noctalia-config-validated.toml" {} ''
+            ${lib.getExe noctalia} config validate ${rawConfig}
+            cp ${rawConfig} $out
+          '';
+        value = {
+          battery.warning_threshold = 20;
+          notification.layer = "overlay";
+          osd = {
+            background_opacity = 0.9;
+            position_vertical = "top_right";
+          };
+          desktop_widgets.enabled = false;
+          shell = {
+            avatar_path = lib.mkIf (user.identity.avatar != null) user.identity.avatar;
+            lang = "zh_CN.UTF-8";
+            panel = {
+              open_near_click_control_center = true;
+              transparency_mode = "glass";
+            };
+          };
+          bar = {
+            order = ["default"];
+            default = {
+              background_opacity = 0.9;
+              margin_edge = 8;
+              margin_ends = 8;
+              radius = 14;
+              start = ["launcher" "privacy" "media"];
+              center = ["workspaces"];
+              end = [
+                "tray"
+                "notifications"
+                "clipboard"
+                "network"
+                "bluetooth"
+                "volume"
+                "brightness"
+                "battery"
+                "clock"
+                "control-center"
+              ];
+            };
+          };
+          widget = {
+            battery = {
+              display_mode = "graphic";
+              show_label = false;
+            };
+            brightness.show_label = false;
+            clock = {
+              capsule = true;
+              capsule_opacity = 0.9;
+              capsule_padding = 10.0;
+              format = "{:%Y-%m-%d %H:%M}";
+            };
+            control-center = {
+              capsule = true;
+              capsule_opacity = 0.9;
+              custom_image = "${pkgs.nixos-icons}/share/icons/hicolor/64x64/apps/nix-snowflake.png";
+            };
+            launcher = {
+              capsule = true;
+              capsule_opacity = 0.9;
+            };
+            media = {
+              capsule = true;
+              capsule_opacity = 0.9;
+            };
+            network.show_label = false;
+            privacy = {
+              capsule = true;
+              capsule_opacity = 0.9;
+            };
+            tray = {drawer = true;};
+            volume.show_label = false;
+            workspaces.hide_when_empty = true;
+          };
+        };
+      };
+    };
+  };
+}
modules/desktop/noctalia/integrations.nix
@@ -0,0 +1,50 @@
+{den, ...}: {
+  den.aspects.desktop.noctalia.includes = [
+    (
+      den.lib.policy.when
+      ({user, ...}: user.hasAspect den.aspects.desktop.wm.niri)
+      den.aspects.desktop.noctalia.integration.niri
+    )
+  ];
+
+  den.aspects.desktop.noctalia.integration = {
+    niri = {
+      hjem = {
+        xdg.config.files."noctalia/config.toml".value = {
+          backdrop.enabled = true;
+        };
+
+        xdg.config.files."niri/nix/pre.kdl".text = ''
+          spawn-at-startup "noctalia"
+          layer-rule {
+          	match namespace="^noctalia-backdrop"
+          	place-within-backdrop true
+          }
+          layer-rule {
+          	match namespace="^noctalia-(bar-[^\"]+|notification|dock|panel|attached-panel|osd)$"
+          	background-effect { xray false; }
+          }
+          window-rule {
+           	match app-id="dev.noctalia.Noctalia.Settings"
+           	open-floating true
+           	background-effect { xray false; }
+           	default-column-width { fixed 1080; }
+           	default-window-height { fixed 920; }
+          }
+        '';
+
+        xdg.config.files."niri/nix/keybinds.kdl".value = {
+          "Mod+Space" = ''spawn-sh  "noctalia msg panel-toggle launcher"'';
+          "Mod+X" = ''spawn-sh "noctalia msg panel-toggle control-center"'';
+          "Mod+V" = ''spawn-sh "noctalia msg panel-toggle clipboard"'';
+          "Mod+Alt+L" = ''spawn-sh "noctalia msg session lock"'';
+          "XF86AudioRaiseVolume" = ''spawn-sh "noctalia msg volume-up"'';
+          "XF86AudioLowerVolume" = ''spawn-sh "noctalia msg volume-down"'';
+          "XF86AudioMute" = ''spawn-sh "noctalia msg volume-mute"'';
+          "XF86MonBrightnessUp" = ''spawn-sh "noctalia msg brightness-up"'';
+          "XF86MonBrightnessDown" = ''spawn-sh "noctalia msg brightness-down"'';
+        };
+      };
+    };
+  };
+}
modules/desktop/wm/niri/config/config.kdl
@@ -0,0 +1,11 @@
+include "nix/pre.kdl"
+
+include "inputs.kdl"
+include "keybinds.kdl"
+include "layout.kdl"
+include "misc.kdl"
+include "window-rules.kdl"
+include "workspaces.kdl"
+
+include "nix/post.kdl"
+
modules/desktop/wm/niri/config/inputs.kdl
@@ -0,0 +1,8 @@
+input {
+  touchpad {
+    tap
+    natural-scroll
+    dwt
+    drag true
+  }
+}
modules/desktop/wm/niri/config/keybinds.kdl
@@ -0,0 +1,129 @@
+binds {
+  // === Common Binds ===
+  Mod+Shift+Slash         { show-hotkey-overlay; }
+  Mod+Escape repeat=false { toggle-overview; }
+
+  Mod+Shift+E     { quit; }
+  Ctrl+Alt+Delete { quit; }
+
+  Print       { screenshot; }
+  Mod+Shift+S { screenshot; }
+  Ctrl+Print  { screenshot-screen; }
+  Alt+Print   { screenshot-window; }
+
+  // === Window and Column Actions ===
+  Mod+Q repeat=false { close-window; }
+
+  Mod+H     { focus-column-left; }
+  Mod+J     { focus-window-down; }
+  Mod+K     { focus-window-up; }
+  Mod+L     { focus-column-right; }
+  Mod+Left  { focus-column-left; }
+  Mod+Down  { focus-window-down; }
+  Mod+Up    { focus-window-up; }
+  Mod+Right { focus-column-right; }
+  Mod+Home  { focus-column-first; }
+  Mod+End   { focus-column-last; }
+  Mod+WheelScrollRight      { focus-column-right; }
+  Mod+WheelScrollLeft       { focus-column-left; }
+  Mod+Shift+WheelScrollDown { focus-column-right; }
+  Mod+Shift+WheelScrollUp   { focus-column-left; }
+
+  Mod+Ctrl+H     { move-column-left; }
+  Mod+Ctrl+J     { move-window-down; }
+  Mod+Ctrl+K     { move-window-up; }
+  Mod+Ctrl+L     { move-column-right; }
+  Mod+Ctrl+Left  { move-column-left; }
+  Mod+Ctrl+Down  { move-window-down; }
+  Mod+Ctrl+Up    { move-window-up; }
+  Mod+Ctrl+Right { move-column-right; }
+  Mod+Ctrl+Home  { move-column-to-first; }
+  Mod+Ctrl+End   { move-column-to-last; }
+  Mod+Ctrl+WheelScrollRight      { move-column-right; }
+  Mod+Ctrl+WheelScrollLeft       { move-column-left; }
+  Mod+Ctrl+Shift+WheelScrollDown { move-column-right; }
+  Mod+Ctrl+Shift+WheelScrollUp   { move-column-left; }
+
+  Mod+Minus       { set-column-width "-10%"; }
+  Mod+Equal       { set-column-width "+10%"; }
+  Mod+Shift+Minus { set-window-height "-10%"; }
+  Mod+Shift+Equal { set-window-height "+10%"; }
+
+  Mod+R       { switch-preset-column-width; }
+  Mod+Shift+R { switch-preset-window-height; }
+  Mod+Ctrl+R  { reset-window-height; }
+
+  Mod+F            { maximize-column; }
+  Mod+Shift+F      { fullscreen-window; }
+  Mod+Ctrl+F       { maximize-window-to-edges; }
+  Mod+Ctrl+Shift+F { toggle-windowed-fullscreen; }
+
+  Mod+BracketLeft  { consume-or-expel-window-left; }
+  Mod+BracketRight { consume-or-expel-window-right; }
+  Mod+Comma        { consume-window-into-column; }
+  Mod+Period       { expel-window-from-column; }
+
+  Mod+Ctrl+W { toggle-window-floating; }
+  Mod+W      { switch-focus-between-floating-and-tiling; }
+
+  Mod+T { toggle-column-tabbed-display; }
+
+  // === Workspace Actions ===
+  Mod+Page_Down { focus-workspace-down; }
+  Mod+Page_Up   { focus-workspace-up; }
+  Mod+D { focus-workspace-down; } // Default is `U`, use Helix binding instead.
+  Mod+U { focus-workspace-up; }   // Default is `I`, use Helix binding instead.
+  Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
+  Mod+WheelScrollUp   cooldown-ms=150 { focus-workspace-up; }
+
+  Mod+Ctrl+Page_Down { move-column-to-workspace-down; }
+  Mod+Ctrl+Page_Up   { move-column-to-workspace-up; }
+  Mod+Ctrl+D { move-column-to-workspace-down; }
+  Mod+Ctrl+U { move-column-to-workspace-up; }
+  Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
+  Mod+Ctrl+WheelScrollUp   cooldown-ms=150 { move-column-to-workspace-up; }
+
+  Mod+Shift+Page_Down { move-workspace-down; }
+  Mod+Shift+Page_Up   { move-workspace-up; }
+  Mod+Shift+D { move-workspace-down; }
+  Mod+Shift+U { move-workspace-up; }
+
+  Mod+1 { focus-workspace 1; }
+  Mod+2 { focus-workspace 2; }
+  Mod+3 { focus-workspace 3; }
+  Mod+4 { focus-workspace 4; }
+  Mod+5 { focus-workspace 5; }
+  Mod+6 { focus-workspace 6; }
+  Mod+7 { focus-workspace 7; }
+  Mod+8 { focus-workspace 8; }
+  Mod+9 { focus-workspace 9; }
+
+  Mod+Ctrl+1 { move-column-to-workspace 1; }
+  Mod+Ctrl+2 { move-column-to-workspace 2; }
+  Mod+Ctrl+3 { move-column-to-workspace 3; }
+  Mod+Ctrl+4 { move-column-to-workspace 4; }
+  Mod+Ctrl+5 { move-column-to-workspace 5; }
+  Mod+Ctrl+6 { move-column-to-workspace 6; }
+  Mod+Ctrl+7 { move-column-to-workspace 7; }
+  Mod+Ctrl+8 { move-column-to-workspace 8; }
+  Mod+Ctrl+9 { move-column-to-workspace 9; }
+
+  // === Monitor Actions ===
+  Mod+Shift+H     { focus-monitor-left; }
+  Mod+Shift+J     { focus-monitor-down; }
+  Mod+Shift+K     { focus-monitor-up; }
+  Mod+Shift+L     { focus-monitor-right; }
+  Mod+Shift+Left  { focus-monitor-left; }
+  Mod+Shift+Down  { focus-monitor-down; }
+  Mod+Shift+Up    { focus-monitor-up; }
+  Mod+Shift+Right { focus-monitor-right; }
+
+  Mod+Ctrl+Shift+H     { move-column-to-monitor-left; }
+  Mod+Ctrl+Shift+J     { move-column-to-monitor-down; }
+  Mod+Ctrl+Shift+K     { move-column-to-monitor-up; }
+  Mod+Ctrl+Shift+L     { move-column-to-monitor-right; }
+  Mod+Ctrl+Shift+Left  { move-column-to-monitor-left; }
+  Mod+Ctrl+Shift+Down  { move-column-to-monitor-down; }
+  Mod+Ctrl+Shift+Up    { move-column-to-monitor-up; }
+  Mod+Ctrl+Shift+Right { move-column-to-monitor-right; }
+}
modules/desktop/wm/niri/config/layout.kdl
@@ -0,0 +1,20 @@
+layout {
+  gaps 10
+  always-center-single-column
+  background-color "transparent"
+
+  focus-ring {
+    on
+    width 2
+  }
+  border {
+    off
+  }
+
+  preset-column-widths {
+    proportion 0.33333
+    proportion 0.5
+    proportion 0.75
+  }
+  default-column-width { proportion 0.5; }
+}
modules/desktop/wm/niri/config/misc.kdl
@@ -0,0 +1,27 @@
+hotkey-overlay {
+  skip-at-startup
+  hide-not-bound
+}
+
+prefer-no-csd
+
+recent-windows {
+  debounce-ms 750
+  open-delay-ms 150
+  highlight {
+    active-color "#cba6f7"
+    urgent-color "#f2cdcd"
+    padding 30
+    corner-radius 15.000000
+  }
+  previews {
+    max-height 480
+    max-scale 0.500000
+  }
+  binds {
+    Alt+Tab { next-window; }
+    Alt+Shift+Tab { previous-window; }
+    Mod+Tab { next-window; }
+    Mod+Shift+Tab { previous-window; }
+  }
+}
modules/desktop/wm/niri/config/window-rules.kdl
@@ -0,0 +1,112 @@
+// rules for all windows
+window-rule {
+  draw-border-with-background false
+  geometry-corner-radius 15.000000 15.000000 15.000000 15.000000
+  clip-to-geometry true
+  tiled-state true
+
+  opacity 0.9
+  background-effect {
+    blur true
+  }
+}
+
+
+// No opacity for a11y
+window-rule {
+  match is-window-cast-target=true
+
+  // Reader
+  match app-id="firefox"
+  match app-id="chromium-browser"
+  match app-id="floorp"
+  match app-id="thunderbird"
+
+  // Multi Media
+  match app-id="org.kde.kdenlive"
+  match app-id="gimp"
+  match app-id="org.inkscape.Inkscape"
+  match app-id="wpsoffice"
+  match app-id="^libreoffice-.*+$"
+  match app-id="blender"
+  match app-id="mpv"
+  match app-id="org.gnome.Loupe"
+  match app-id="atril"
+
+  opacity 1.0
+}
+
+
+// Maximize
+window-rule {
+  match app-id="code"
+  match app-id="dev.zed.Zed"
+  match app-id="firefox"
+  match app-id="floorp"
+  match app-id="chromium-browser"
+  match app-id="thunderbird"
+  match app-id="CherryStudio"
+  match app-id="deadbeef"
+  match app-id="mpv"
+  match app-id="steam" title="^Steam$"
+  match app-id="org.kde.kdenlive"
+  match app-id="gimp" is-floating=false
+  match app-id="wpsoffice" title="^WPS Office$"
+
+  open-maximized true
+}
+
+// Floating
+window-rule {
+  match app-id="QQ" title="^(ๅ›พ็‰‡ๆŸฅ็œ‹ๅ™จ|ๆ–‡ไปถ็ฎก็†ๅ™จ|ๆ”ถ่—|่Šๅคฉ่ฎฐๅฝ•็ฎก็†|่ฎพ็ฝฎ|็พค็›ธๅ†Œ - .*?)$"
+
+  open-floating true
+}
+
+// === Workspaces ===
+window-rule {
+  match app-id="code"
+  match app-id="dev.zed.Zed"
+  match app-id="org.kde.kdenlive"
+  match app-id="gimp"
+  match app-id="wpsoffice"
+
+  open-on-workspace "1work"
+}
+
+window-rule {
+  match app-id="firefox"
+  match app-id="chromium-browser"
+  match app-id="floorp"
+
+  open-on-workspace "2web"
+}
+
+window-rule {
+  match app-id="org.telegram.desktop"
+  match app-id="wechat"
+  match app-id="QQ"
+  match app-id="thunderbird"
+  match app-id="CherryStudio"
+  match app-id="discord"
+  match app-id="vesktop"
+  match app-id="fluffychat"
+  match app-id="com.psyche.kelivo"
+
+  open-on-workspace "3chat"
+}
+
+window-rule {
+  match app-id="org.prismlauncher.PrismLauncher"
+  match app-id="steam"
+  match app-id="net.lutris.Lutris"
+  match app-id="heroic"
+  match app-id="moe.launcher.an-anime-game-launcher"
+  match app-id="moe.launcher.honkers-railway-launcher"
+  match app-id="moe.launcher.sleepy-launcher"
+  match app-id="yuanshen.exe"
+  match app-id="starrail.exe"
+  match app-id="zenlesszonezero.exe"
+
+  open-on-workspace "4game"
+}
modules/desktop/wm/niri/config/workspaces.kdl
@@ -0,0 +1,4 @@
+workspace "1work"
+workspace "2web"
+workspace "3chat"
+workspace "4game"
modules/desktop/wm/niri/default.nix
@@ -0,0 +1,51 @@
+{inputs, ...}: {
+  den.aspects.desktop.wm.niri = {
+    nixos = {
+      # See https://github.com/NixOS/nixpkgs/blob/bcb52fa87abbc7ec046c4817d85950c35dae92b7/nixos/modules/services/misc/graphical-desktop.nix
+      services.graphical-desktop.enable = true;
+
+      programs.niri = {
+        enable = true;
+        useNautilus = true;
+      };
+    };
+
+    provides.to-users = {
+      hjem = {
+        user,
+        pkgs,
+        lib,
+        ...
+      }: let
+        niriKeybindsKDLGenerator = binds: ''
+          binds {
+            ${lib.concatMapAttrsStringSep "\n  " (key: bind: ''${key} { ${bind}; }'') binds}
+          }
+        '';
+      in {
+        xdg.config.files =
+          lib.mergeAttrsList
+          (inputs.import-tree.withLib lib
+            (i: i.initFilter (lib.hasSuffix ".kdl"))
+            (i: i.map (x: {"niri/${lib.lists.last (lib.path.subpath.components (lib.path.splitRoot x).subpath)}".source = x;}))
+            (i: i.leafs ./config))
+          // {
+            "niri/nix/pre.kdl".text = "";
+            "niri/nix/post.kdl".text = ''
+              include "keybinds.kdl"
+
+              xwayland-satellite { path "${lib.getExe pkgs.xwayland-satellite}"; }
+            '';
+            "niri/nix/keybinds.kdl" = {
+              generator = niriKeybindsKDLGenerator;
+              value = {
+                "Mod+Grave" =
+                  lib.mkIf (user.system.settings.desktop.app.term.default != null)
+                  ''spawn "${user.system.settings.desktop.app.term.default}"'';
+              };
+            };
+          };
+      };
+    };
+  };
+}
modules/desktop/xdg-user-dirs/_hjem-module.nix
@@ -0,0 +1,47 @@
+{
+  lib,
+  config,
+  pkgs,
+  ...
+}: {
+  options = {
+    xdg.user-dirs = {
+      enable = lib.mkEnableOption "Whether to manage `$XDG_CONFIG_HOME/user-dirs.dirs`";
+      package = lib.mkPackageOption pkgs "xdg-user-dirs" {nullable = true;};
+      setSessionVariables = lib.mkEnableOption "Whether to set the XDG user dir environment variables";
+      directories = lib.mkOption {
+        type = lib.types.lazyAttrsOf lib.types.str;
+        description = "All user directories";
+        default = {
+          desktop = "${config.directory}/Desktop";
+          documents = "${config.directory}/Documents";
+          download = "${config.directory}/Downloads";
+          music = "${config.directory}/Music";
+          pictures = "${config.directory}/Pictures";
+          projects = "${config.directory}/Projects";
+          publicShare = "${config.directory}/publicShare";
+          templates = "${config.directory}/Templates";
+          videos = "${config.directory}/Videos";
+        };
+      };
+    };
+  };
+  config = let
+    cfg = config.xdg.user-dirs;
+    mkXDGVariable = dirs:
+      lib.mapAttrs'
+      (dir: path: lib.nameValuePair "XDG_${lib.strings.toUpper dir}_DIR" path)
+      dirs;
+  in
+    lib.mkIf cfg.enable {
+      packages = lib.optional (cfg.package != null) cfg.package;
+
+      xdg.config.files."user-dirs.dirs" = {
+        generator = lib.generators.toKeyValue {};
+        value = lib.mapAttrs (_: dir: ''"${dir}"'') (mkXDGVariable cfg.directories);
+      };
+      xdg.config.files."user-dirs.conf".text = "enabled=False";
+
+      environment.sessionVariables = lib.mkIf cfg.setSessionVariables (mkXDGVariable cfg.directories);
+    };
+}
modules/desktop/xdg-user-dirs/default.nix
@@ -0,0 +1,58 @@
+{den, ...}: {
+  den.aspects.desktop.includes = [den.aspects.desktop.xdg-user-dirs];
+  den.aspects.desktop.xdg-user-dirs = {
+    nixos = {
+      environment.variables = {
+        XCOMPOSECACHE = "$XDG_CACHE_HOME/X11/xcompose";
+      };
+    };
+
+    provides.to-users = {
+      includes = [
+        {
+          persistHome = {config, ...}: {
+            directories = with config.xdg.user-dirs.directories; [
+              desktop
+              documents
+              music
+              pictures
+              projects
+              publicShare
+              templates
+              videos
+            ];
+          };
+        }
+        {
+          cacheHome = {config, ...}: {
+            directories = [
+              config.xdg.user-dirs.directories.download
+            ];
+          };
+        }
+      ];
+
+      hjem = {config, ...}: {
+        imports = [./_hjem-module.nix];
+
+        xdg.user-dirs = {
+          enable = true;
+          setSessionVariables = true;
+          directories = {
+            desktop = "${config.directory}/Desktop";
+            documents = "${config.directory}/Documents";
+            download = "${config.directory}/Downloads";
+            music = "${config.directory}/Musics";
+            pictures = "${config.directory}/Pictures";
+            projects = "${config.directory}/Development";
+            videos = "${config.directory}/Videos";
+            publicShare = "${config.directory}/Share";
+            templates = "${config.directory}/Templates";
+
+            screenshots = "${config.xdg.user-dirs.directories.pictures}/Screenshots";
+          };
+        };
+      };
+    };
+  };
+}
modules/desktop/flatpak.nix
@@ -0,0 +1,86 @@
+{
+  inputs,
+  lib,
+  ...
+}: {
+  flake-file.inputs.nix-flatpak = {
+    url = "github:gmodena/nix-flatpak";
+  };
+
+  den.aspects.desktop.flatpak = {
+    settings = let
+      flatpakOption = {
+        packages = lib.mkOption {
+          type = lib.types.listOf (lib.types.either lib.types.str lib.types.attr);
+          default = [];
+        };
+        overrides = lib.mkOption {
+          type = lib.types.attrsOf (lib.types.attrsOf lib.types.anything);
+          default = {};
+        };
+      };
+    in {
+      host = flatpakOption;
+      user = flatpakOption;
+    };
+
+    cache = {
+      directories = [
+        "/var/lib/flatpak"
+      ];
+    };
+
+    persistHome = {
+      directories = [
+        ".var" # Flatpak app data
+      ];
+    };
+
+    cacheHome = {config, ...}: {
+      directories = [
+        "${config.xdg.data.directory}/flatpak"
+      ];
+    };
+
+    nixos = {host, ...}: {
+      imports = [inputs.nix-flatpak.nixosModules.nix-flatpak];
+
+      services.flatpak = {
+        enable = true;
+
+        inherit (host.settings.desktop.flatpak) packages;
+      };
+    };
+
+    hjem = {user, ...}: {
+      imports = [inputs.nix-flatpak.hjemModules.nix-flatpak];
+
+      services.flatpak = {
+        enable = true;
+
+        packages = user.system.settings.desktop.flatpak.packages;
+
+        overrides.settings = lib.mergeAttrsList [
+          {
+            global = {
+              Context.filesystems = [
+                "/nix/store:ro"
+                "/run/current-system/sw/share/X11/fonts:ro"
+                "xdg-data/fonts:ro"
+                "home/.icons:ro"
+                "xdg-config/gtk-3.0/gtk.css:ro"
+                "xdg-config/gtk-4.0/gtk.css:ro"
+              ];
+            };
+          }
+          (user.system.settings.desktop.flatpak.overrides)
+        ];
+      };
+
+      xdg.data.files."fonts" = {
+        type = "symlink";
+        source = "/run/current-system/sw/share/X11/fonts";
+      };
+    };
+  };
+}
modules/desktop/gnupg.nix
@@ -0,0 +1,178 @@
+{den, ...}: {
+  den.aspects.desktop.includes = [den.aspects.desktop.gnupg];
+  den.aspects.desktop.gnupg = {
+    nixos = {pkgs, ...}: {
+      programs.gnupg.agent = {
+        enable = true;
+        pinentryPackage = pkgs.pinentry-gnome3;
+        enableSSHSupport = false;
+        settings.default-cache-ttl = 4 * 60 * 60; # 4 hours
+      };
+    };
+
+    provides.to-users = {
+      includes = [
+        {
+          persistHome = {config, ...}: {
+            directories = [
+              {
+                directory = config.environment.sessionVariables.GNUPGHOME;
+                mode = "0700";
+              }
+            ];
+          };
+        }
+      ];
+
+      hjem = {
+        user,
+        pkgs,
+        lib,
+        config,
+        ...
+      }: let
+        # Port of home-manager's programs.gpg
+        # See https://github.com/nix-community/home-manager/blob/c53d643b3737e2fcd04e6cb3b3580ef50b2087a0/modules/programs/gpg.nix
+        gpgConfGenerator = value:
+          lib.generators.toKeyValue {
+            mkKeyValue = key: v:
+              if lib.isString v
+              then "${key} ${v}"
+              else lib.optionalString v key;
+            listsAsDuplicateKeys = true;
+          }
+          value;
+
+        # Managed public keys, all ultimately trusted.
+        publicKeys =
+          map (key: {
+            trust = 5;
+            source =
+              if builtins.isPath key
+              then key
+              else pkgs.writeText "gpg-pubkey" key;
+          })
+          user.identity.gpgKeys;
+
+        # Build the immutable keyring with the managed keys imported.
+        gpgKeyring = pkgs.runCommand "gpg-pubring" {buildInputs = [pkgs.gnupg];} (
+          let
+            gpg = "${pkgs.gnupg}/bin/gpg";
+
+            importKey = {
+              source,
+              trust,
+              ...
+            }: ''
+              ${gpg} --import ${source}
+              ${lib.optionalString (trust != null) ''importTrust "${source}" ${toString trust}''}
+            '';
+
+            importKeys = lib.concatMapStringsSep "\n" importKey publicKeys;
+          in ''
+            GNUPGHOME=$(mktemp -d)
+            export GNUPGHOME
+
+            function gpgKeyId() {
+              ${gpg} --show-key --with-colons "$1" \
+                | grep ^pub: \
+                | cut -d: -f5
+            }
+
+            function importTrust() {
+              local keyIds trust
+              mapfile -t keyIds <<< "$(gpgKeyId "$1")"
+              trust="$2"
+              for id in "''${keyIds[@]}"; do
+                { echo trust; echo "$trust"; (( trust == 5 )) && echo y; echo quit; } \
+                  | ${gpg} --no-tty --command-fd 0 --edit-key "$id"
+              done
+            }
+
+            ${importKeys}
+
+            mkdir $out
+            cp $GNUPGHOME/pubring.kbx $out/pubring.kbx
+            if [[ -e $GNUPGHOME/trustdb.gpg ]] ; then
+              cp $GNUPGHOME/trustdb.gpg $out/trustdb.gpg
+            fi
+          ''
+        );
+      in {
+        packages = [pkgs.gnupg];
+
+        environment.sessionVariables.GNUPGHOME = "${config.directory}/.gnupg";
+
+        files =
+          {
+            # This configuration is based on the tutorial below, it allows for a robust setup
+            # https://blog.eleven-labs.com/en/openpgp-almost-perfect-key-pair-part-1
+            # ~/.gnupg/gpg.conf
+            ".gnupg/gpg.conf" = {
+              generator = gpgConfGenerator;
+              value = {
+                # Get rid of the copyright notice
+                no-greeting = true;
+
+                # --- Avoid information leaked --- #
+                # Disable inclusion of the version string in ASCII armored output
+                no-emit-version = true;
+                # Do not write comment packets
+                no-comments = false;
+                # Export the smallest key possible
+                # This removes all signatures except the most recent self-signature on each user ID
+                export-options = "export-minimal";
+
+                # Display long key IDs
+                keyid-format = "0xlong";
+                # List all keys (or the specified ones) along with their fingerprints
+                with-fingerprint = true;
+
+                # Display the calculated validity of user IDs during key listings
+                list-options = "show-uid-validity";
+                verify-options = "show-uid-validity show-keyserver-urls";
+
+                # Select the strongest cipher
+                personal-cipher-preferences = "AES256";
+                # Select the strongest digest
+                personal-digest-preferences = "SHA512";
+                # This preference list is used for new keys and becomes the default for "setpref" in the edit menu
+                default-preference-list = "SHA512 SHA384 SHA256 RIPEMD160 AES256 TWOFISH BLOWFISH ZLIB BZIP2 ZIP Uncompressed";
+
+                # Use the strongest cipher algorithm
+                cipher-algo = "AES256";
+                # Use the strongest digest algorithm
+                digest-algo = "SHA512";
+                # Message digest algorithm used when signing a key
+                cert-digest-algo = "SHA512";
+                # Use RFC-1950 ZLIB compression
+                compress-algo = "ZLIB";
+
+                # Disable weak algorithm
+                disable-cipher-algo = "3DES";
+                # Treat the specified digest algorithm as weak
+                weak-digest = "SHA1";
+
+                # The cipher algorithm for symmetric encryption for symmetric encryption with a passphrase
+                s2k-cipher-algo = "AES256";
+                # The digest algorithm used to mangle the passphrases for symmetric encryption
+                s2k-digest-algo = "SHA512";
+                # Selects how passphrases for symmetric encryption are mangled
+                s2k-mode = "3";
+                # Specify how many times the passphrases mangling for symmetric encryption is repeated
+                s2k-count = "65011712";
+              };
+            };
+          }
+          // lib.optionalAttrs (publicKeys != []) {
+            # Immutable keyring: managed keys/trust are linked from the store.
+            ".gnupg/pubring.kbx".source = "${gpgKeyring}/pubring.kbx";
+            ".gnupg/trustdb.gpg" = {
+              type = "copy";
+              source = "${gpgKeyring}/trustdb.gpg";
+            };
+          };
+      };
+    };
+  };
+}
modules/desktop/greeter.nix
@@ -0,0 +1,10 @@
+{den, ...}: {
+  den.aspects.desktop.includes = [den.aspects.desktop.greeter];
+  den.aspects.desktop.greeter = {
+    nixos = {
+      services.displayManager.ly = {
+        enable = true;
+      };
+    };
+  };
+}
modules/desktop/kmscon.nix
@@ -0,0 +1,14 @@
+{den, ...}: {
+  den.aspects.desktop.includes = [den.aspects.desktop.kmscon];
+  den.aspects.desktop.kmscon = {
+    nixos = {config, ...}: {
+      services.kmscon = {
+        enable = true;
+        extraOptions = "--term xterm-256color";
+        config = {
+          hwaccel = config.hardware.facter.detected.graphics.enable or false;
+        };
+      };
+    };
+  };
+}
modules/desktop/locale.nix
@@ -0,0 +1,35 @@
+{den, ...}: {
+  den.aspects.desktop.includes = [den.aspects.desktop.locale];
+  den.aspects.desktop.locale = {
+    nixos = {
+      lib,
+      config,
+      ...
+    }: {
+      i18n.supportedLocales = lib.unique [
+        "C.UTF-8/UTF-8"
+        "en_US.UTF-8/UTF-8"
+        "${config.i18n.defaultLocale}/UTF-8"
+        "zh_CN.UTF-8/UTF-8"
+      ];
+    };
+
+    provides.to-users.hjem = let
+      locale = "zh_CN.UTF-8";
+    in {
+      xdg.config.files."environment.d/60-graphical-locale.conf".text = ''
+        LANG=${locale}
+        LANGUAGE=${locale}
+        LC_ADDRESS=${locale}
+        LC_IDENTIFICATION=${locale}
+        LC_MEASUREMENT=${locale}
+        LC_MONETARY=${locale}
+        LC_NAME=${locale}
+        LC_NUMERIC=${locale}
+        LC_PAPER=${locale}
+        LC_TELEPHONE=${locale}
+        LC_TIME=${locale}
+      '';
+    };
+  };
+}
modules/desktop/oo7.nix
@@ -0,0 +1,26 @@
+{den, ...}: {
+  den.aspects.desktop.includes = [den.aspects.desktop.oo7];
+  den.aspects.desktop.oo7 = {
+    provides.to-users.includes = [
+      {
+        persistHome = {config, ...}: {
+          directories = [
+            "${config.xdg.data.directory}/keyrings"
+          ];
+        };
+      }
+    ];
+
+    nixos = {
+      services.oo7.enable = true;
+      services.gnome.gnome-keyring.enable = false;
+
+      xdg.portal = {
+        enable = true;
+        config.common."org.freedesktop.impl.portal.Secret" = [
+          "oo7"
+        ];
+      };
+    };
+  };
+}
modules/desktop/pipewire.nix
@@ -0,0 +1,29 @@
+{den, ...}: {
+  den.aspects.desktop.includes = [den.aspects.desktop.pipewire];
+  den.aspects.desktop.pipewire = {
+    provides.to-users.includes = [
+      {
+        cacheHome = {config, ...}: {
+          directories = [
+            "${config.xdg.state.directory}/wireplumber"
+          ];
+        };
+      }
+    ];
+
+    nixos = {
+      services.pipewire = {
+        enable = true;
+        alsa.enable = true;
+        alsa.support32Bit = true;
+        pulse.enable = true;
+        jack.enable = true;
+        wireplumber.enable = true;
+      };
+
+      security.rtkit.enable = true;
+
+      services.pulseaudio.enable = false;
+    };
+  };
+}
modules/desktop/power.nix
@@ -0,0 +1,24 @@
+{den, ...}: {
+  den.aspects.desktop.includes = [den.aspects.desktop.power];
+  den.aspects.desktop.power = {
+    persist = {
+      directories = [
+        "/var/lib/upower"
+      ];
+    };
+
+    nixos = {pkgs, ...}: {
+      services = {
+        tlp = {
+          enable = true;
+          pd.enable = true;
+        };
+        upower.enable = true;
+      };
+
+      environment.systemPackages = [
+        pkgs.powertop # Using package instead of module to avoid USB auto suspend
+      ];
+    };
+  };
+}
modules/desktop/utils.nix
@@ -0,0 +1,27 @@
+{den, ...}: {
+  den.aspects.desktop.includes = [den.aspects.desktop.utils];
+  den.aspects.desktop.utils = {
+    nixos = {pkgs, ...}: {
+      environment.systemPackages = with pkgs; [
+        wl-clipboard-rs
+
+        # Thumbnail
+        gdk-pixbuf
+        libheif
+        webp-pixbuf-loader
+        ffmpeg-headless
+        ffmpegthumbnailer
+      ];
+    };
+
+    provides.to-users.hjem = {pkgs, ...}: {
+      systemd.services.polkit-gnome = {
+        description = "GNOME PolicyKit Agent";
+        partOf = ["graphical-session.target"];
+        after = ["graphical-session.target"];
+        wantedBy = ["graphical-session.target"];
+        script = "${pkgs.polkit_gnome}/libexec/polkit-gnome-authentication-agent-1";
+      };
+    };
+  };
+}
modules/desktop/yubikey.nix
@@ -0,0 +1,17 @@
+{
+  den.aspects.desktop.yubikey = {
+    nixos = {pkgs, ...}: {
+      services.udev.packages = [pkgs.yubikey-personalization];
+      hardware.gpgSmartcards.enable = true;
+      services.pcscd.enable = true;
+
+      programs.yubikey-manager.enable = true;
+
+      programs.yubikey-touch-detector.enable = true;
+    };
+
+    hjem = {
+      files.".gnupg/scdaemon.conf".text = "disable-ccid";
+    };
+  };
+}
modules/dev/jujutsu.nix
@@ -1,5 +1,10 @@
-{
+{den, ...}: {
   den.aspects.dev.jujutsu = {
+    includes =
+      den.lib.policy.when
+      ({host, ...}: host.hasAspect den.aspects.desktop.gnupg)
+      [den.aspects.dev.jujutsu.sign];
+
     provides.to-users = {
       hjem = {
         user,
@@ -23,4 +28,18 @@
       };
     };
   };
+
+  den.aspects.dev.jujutsu.sign = {
+    hjem = {
+      xdg.config.files."jj/config.toml".value = {
+        signing = {
+          behavior = "drop";
+          backend = "gpg";
+        };
+        git = {
+          sign-on-push = true;
+        };
+      };
+    };
+  };
 }
modules/hosts/kevin/default.nix
@@ -17,6 +17,8 @@
 
   den.aspects.kevin = {
     includes = with den.aspects; [
+      desktop
+      desktop.game
       dev
     ];
   };
modules/hosts/kevin/hardware.nix
@@ -14,5 +14,34 @@
 
       boot.loader.systemd-boot.enable = true;
     };
+
+    provides.to-users = {
+      hjem = {
+        pkgs,
+        lib,
+        config,
+        ...
+      }: {
+        packages = [pkgs.kanshi];
+
+        xdg.config.files."kanshi/config".source = ./kanshi-config;
+
+        systemd.services.kanshi = {
+          description = "Dynamic output configuration";
+          documentation = ["man:kanshi(1)"];
+
+          partOf = ["graphical-session.target"];
+          requires = ["graphical-session.target"];
+          after = ["graphical-session.target"];
+          wantedBy = ["graphical-session.target"];
+
+          script = lib.getExe pkgs.kanshi;
+          reload = "${lib.getExe' pkgs.kanshi "kanshictl"} reload";
+          reloadTriggers = ["${config.xdg.config.directory}/kanshi/config"];
+
+          serviceConfig.Restart = "always";
+        };
+      };
+    };
   };
 }
modules/hosts/kevin/kanshi-config
@@ -0,0 +1,11 @@
+output "AU Optronics 0xF1A7 Unknown" mode 3200x2000@165.002 position 0,0 scale 2.000000 alias $INTERNAL
+output "Dell Inc. BoltSnake 8R33926O00QS" mode 2160x1440@60.002 position 0,0 scale 1.500000 alias $PORTABLE
+
+profile single {
+  output "$INTERNAL" scale 2.000000
+}
+
+profile home {
+  output "$INTERNAL" scale 2.000000
+  output "$PORTABLE" position 1600,0 scale 2.000000 transform 270
+}
modules/users/hpcesia/avatar.png
Binary file
modules/users/hpcesia/default.nix
@@ -3,10 +3,14 @@
     identity = {
       displayName = "HPCesia";
       email = "me@hpcesia.com";
+      avatar = ./avatar.png;
       sshKeys = [
         "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFMKaGC2I3an4AJDeWzVx5vhm63+kxi6zJNdh7yEp6CK hpcesia@kevin"
         "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIH6wZFUEv9pSV+MgzqZRJ08WwJvL5FRMhayp73kCnDckAAAABHNzaDo= me@hpcesia.com"
       ];
+      gpgKeys = [
+        ./gpg.asc
+      ];
     };
     system = {
       hashedPasswordAged = ./hashed-password.age;
modules/users/hpcesia/desktop.nix
@@ -0,0 +1,58 @@
+{den, ...}: {
+  den.users.hpcesia = {
+    system.settings = {
+      desktop = {
+        app = {
+          browser.defaults = ["floorp.desktop"];
+          term.default = "foot";
+        };
+        flatpak = {
+          packages = [
+            "com.qq.QQ"
+            "org.mozilla.thunderbird"
+          ];
+          overrides = {
+            "org.mozilla.thunderbird".Context = {
+              sockets = [
+                "gpg-agent" # Expose GPG agent
+                "pcsc" # Expose smart cards
+              ];
+            };
+          };
+        };
+      };
+    };
+  };
+
+  den.aspects.hpcesia = {
+    includes = let
+      includeWhen = cond: aspects:
+        map (aspect: (den.lib.policy.when cond aspect)) aspects;
+    in
+      (includeWhen ({host, ...}: host.hasAspect den.aspects.desktop) (with den.aspects; [
+        desktop.app.browser.chromium
+        desktop.app.browser.floorp
+        desktop.app.imv
+        desktop.app.kdeconnect
+        desktop.app.kdenlive
+        desktop.app.libreoffice
+        desktop.app.mpv
+        desktop.app.obs-studio
+        desktop.app.picard
+        desktop.app.term.foot
+        desktop.flatpak
+        desktop.ime.fcitx5
+        desktop.noctalia
+        desktop.yubikey
+        desktop.wm.niri
+      ]))
+      ++ (includeWhen ({host, ...}: host.hasAspect den.aspects.desktop && host.hasAspect den.aspects.desktop.game) (with den.aspects; [
+        desktop.game.heroic
+        desktop.game.hoyo
+        desktop.game.ludusavi
+        desktop.game.mangohud
+        desktop.game.minecraft
+        desktop.game.steam
+      ]));
+  };
+}
modules/users/hpcesia/gpg.asc
@@ -0,0 +1,28 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mDMEahCUrRYJKwYBBAHaRw8BAQdAKXQUDTZq1Dsu6EffnhrmyOBunSHJEp+pH+rM
+QSWB2FK0GEhQQ2VzaWEgPG1lQGhwY2VzaWEuY29tPokBkwQTFgoBOwIbAQULCQgH
+AwUVCgkICwUWAgMBAAIeAQIXgBYhBKaONCdbG5LgeY8Hyp1lkviuhDOWBQJqEKhC
+WRSAAAAAABAAQHByb29mQGFyaWFkbmUuaWRodHRwczovL2dpc3QuZ2l0aHViLmNv
+bS9IUENlc2lhL2RiODhkZmQ1NDAxMWMyZWQwNzNhZDJkYjhmYTZmNGMwMRSAAAAA
+ABAAGHByb29mQGFyaWFkbmUuaWRkbnM6aHBjZXNpYS5jb20/dHlwZT1UWFQxFIAA
+AAAAEAAYcHJvb2ZAYXJpYWRuZS5pZGh0dHBzOi8vbXljZS5saS9AaHBjZXNpYUQU
+gAAAAAAQACtwcm9vZkBhcmlhZG5lLmlkaHR0cHM6Ly9jb2RlYmVyZy5vcmcvSFBD
+ZXNpYS9rZXlveGlkZV9wcm9vZgAKCRCdZZL4roQzltg4AP0Qd5ju73GvAibr1JpM
+MS8MIUKJD2QsEK9GIxqpdFzivwD9HMv/IqOKYaBD9ysctSrCkzd9IDahJ7o/Icyk
+Jk4xqQm4MwRqEJUTFgkrBgEEAdpHDwEBB0AQDrLl59MZr9GaBk2BGNhaOmDHSVRM
++6rRDpXuVfNMa4j1BBgWCgAmAhsCFiEEpo40J1sbkuB5jwfKnWWS+K6EM5YFAmpS
+CeAFCQCh2K8AgXYgBBkWCgAdFiEEXa0VAC4dje6nH8i1rl8ugv65lqcFAmoQlRMA
+CgkQrl8ugv65lqd8sQD/QZNMz0I4aNKTGYTRwoUT7/vGiRq2yot/as/8sJ1EWmcA
+/2bb8PDBl0f2e6cpe1xcJ3hU9enX9SkcTyTItTyXgscCCRCdZZL4roQzlvn7AP0b
+Dk8wlol5xAyAoxd9jRXEWHzhEU/D9642xjF18ApxHgD7BR7g2kCRvbnB58vxz2Xn
+1q1WlYT48BW9a0QhP23LAQu4OARqEJU0EgorBgEEAZdVAQUBAQdAUyPlpoIiSLV5
+csAeYwzvobHfepOEgKsGBbs9F46ivSYDAQgHiH4EGBYKACYCGwwWIQSmjjQnWxuS
+4HmPB8qdZZL4roQzlgUCalIJ4AUJAKHYjgAKCRCdZZL4roQzloNMAPoCUdXOVZol
+WFt3TKY7n8NARA1wQ1OLy+DXcf272E+ZUgEAhJfxUcGvvsCocoQ9cSeaSIikeIkn
+vA6kv1VW4/1oUAi4MwRqEJWqFgkrBgEEAdpHDwEBB0CQpkWh9chbMv21RBh4Kboz
+p7W0ypREwcd8mSozK97/mYh+BBgWCgAmAhsgFiEEpo40J1sbkuB5jwfKnWWS+K6E
+M5YFAmpSCeAFCQCh2BgACgkQnWWS+K6EM5awDQD5Ad5UOJu4Qd+njI0sVAdAUDVE
+NRafHzxsJHyA4iAzonEA/iunq2XN27Wz91vtl0LtEhHoT5EHASNBlpFy3PcGWDQG
+=xlVU
+-----END PGP PUBLIC KEY BLOCK-----
modules/users/schema.nix
@@ -24,10 +24,20 @@
                   default = null;
                   description = "Email address for the user";
                 };
+                avatar = lib.mkOption {
+                  type = lib.types.nullOr lib.types.path;
+                  default = null;
+                  description = "Avatar for the user";
+                };
                 sshKeys = lib.mkOption {
                   type = lib.types.listOf lib.types.str;
                   default = [];
                 };
+                gpgKeys = lib.mkOption {
+                  type = lib.types.listOf (lib.types.either lib.types.path lib.types.str);
+                  default = [];
+                  description = "OpenPGP public keys (paths or armored key text) for this user";
+                };
               };
             };
             default = {};
flake.lock
@@ -219,6 +219,21 @@
         "type": "github"
       }
     },
+    "nix-flatpak": {
+      "locked": {
+        "lastModified": 1783368811,
+        "narHash": "sha256-0H8jDwR4Kegb3heaTrH1ftbgKfZVDT8JE+46uXxDy/Q=",
+        "owner": "gmodena",
+        "repo": "nix-flatpak",
+        "rev": "20d42f0ee98c9fe9f85e8d1de474f1409ed10d05",
+        "type": "github"
+      },
+      "original": {
+        "owner": "gmodena",
+        "repo": "nix-flatpak",
+        "type": "github"
+      }
+    },
     "nixpkgs": {
       "locked": {
         "lastModified": 1787135253,
@@ -328,6 +343,7 @@
         "hjem": "hjem",
         "import-tree": "import-tree",
         "ncro": "ncro",
+        "nix-flatpak": "nix-flatpak",
         "nixpkgs": "nixpkgs",
         "nur": "nur",
         "nur-hpcesia": "nur-hpcesia",
flake.nix
@@ -37,6 +37,7 @@
       url = "github:manic-systems/ncro";
       inputs.nixpkgs.follows = "nixpkgs";
     };
+    nix-flatpak.url = "github:gmodena/nix-flatpak";
     nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
     nur = {
       url = "github:nix-community/NUR";
README.md
@@ -6,7 +6,8 @@ configuration for all my machines, powered by [den](https://github.com/denful/de
 ## LLM Usage Statement
 
 The code regarding the internal mechanisms of the den framework was basically all
-written by LLMs.
+written by LLMs. A portion of some [Hjem](https://github.com/feel-co/hjem) modules was
+ported from [Home Manager](https://github.com/nix-community/home-manager) using LLMs.
 
 ## Acknowledgements