Commit 07d8739
Changed files (57)
modules
desktop
app
game
ime
noctalia
wm
xdg-user-dirs
dev
hosts
users
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