diff --git a/flake.nix b/flake.nix
index 2b34a84..bd88c45 100644
--- a/flake.nix
+++ b/flake.nix
@@ -22,7 +22,7 @@
inherit inputs;
src = ./.;
- snowfall.namespace = "hh-config";
+ snowfall.namespace = "plusultra";
};
in
lib.mkFlake {
diff --git a/homes/x86_64-linux/harald@nix.fedora.fritz.box/default.nix b/homes/x86_64-linux/harald@nix.fedora.fritz.box/default.nix
index 0fba931..7aefea2 100644
--- a/homes/x86_64-linux/harald@nix.fedora.fritz.box/default.nix
+++ b/homes/x86_64-linux/harald@nix.fedora.fritz.box/default.nix
@@ -10,348 +10,28 @@
# Home Manager needs a bit of information about you and the paths it should
# manage.
home.username = "harald";
- home.homeDirectory = "/home/harald/nix";
+ home.homeDirectory = "/home/${config.home.username}/nix";
home.stateVersion = "23.11"; # Please read the comment before changing.
- home.sessionPath = [
- "$HOME/.local/share/JetBrains/Toolbox/scripts"
- "$HOME/bin"
- ];
+ home.sessionPath = [ "$HOME/bin" ];
- # The home.packages option allows you to install Nix packages into your
- # environment.
- home.packages = with pkgs; [
- bashInteractive
- cachix
- jetbrains-toolbox
- delta
- git
- vim
- tmux
- (pkgs.nerdfonts.override { fonts = [ "FiraCode" "DroidSansMono" "JetBrainsMono" ]; })
- # # You can also create simple shell scripts directly inside your
- # # configuration. For example, this adds a command 'my-hello' to your
- # # environment:
- # (pkgs.writeShellScriptBin "my-hello" ''
- # echo "Hello, ${config.home.username}!"
- # '')
- ];
+ plusultra = {
+ cli-apps = {
+ bash.enable = true;
+ fish.enable = true;
+ neovim.enable = true;
+ tmux.enable = true;
+ bat.enable = true;
+ git.enable = true;
+ starship.enable = true;
+ home-manager.enable = true;
- home.sessionVariables = {
- EDITOR = "vim";
- };
-
- home.shellAliases = {
- cat = "${pkgs.bat}/bin/bat --decorations never";
- less = ''${pkgs.bat}/bin/bat --decorations never --paging=always --pager "${pkgs.less}/bin/less -RF"'';
- man = "${pkgs.bat-extras.batman}/bin/batman";
+ };
+ tools = {
+ direnv.enable = true;
+ jetbrains.enable = true;
+ };
};
fonts.fontconfig.enable = true;
-
- programs = {
- bash = {
- enable = true;
- };
- bat = {
- enable = true;
- config.theme = "ansi";
- extraPackages = with pkgs.bat-extras; [ batdiff batman batgrep batwatch ];
- };
- git = {
- enable = true;
- userName = "Harald Hoyer";
- userEmail = "harald@hoyer.xyz";
- extraConfig = {
- init.defaultBranch = "main";
- "credential \"https://github.com\"" = {
- helper = "!gh auth git-credential";
- };
- alias = {
- co = "checkout";
- ci = "commit";
- };
- pull.ff = "only";
- core.pager = "delta";
- delta = {
- features = "side-by-side line-numbers decorations";
- syntax-theme = "Dracula";
- };
- interactive.diffFilter = "delta --color-only";
- merge.conflictStyle = "diff3";
- diff.colorMoved = "default";
- };
- };
- direnv = {
- enable = true;
- nix-direnv.enable = true;
- };
- fish.enable = true;
- fish.interactiveShellInit = ''
- function msh --wraps mosh --description 'mosh with tmux'
- if not set -q argv[1]
- echo 'Usage: msh [user@]host [command]'
- else
- ${pkgs.mosh}/bin/mosh $argv -- tmux new-session -A -s 0
- end
- end
- '';
-
- fish.plugins = [{
- name = "foreign-env";
- src = pkgs.fetchFromGitHub {
- owner = "oh-my-fish";
- repo = "plugin-foreign-env";
- rev = "dddd9213272a0ab848d474d0cbde12ad034e65bc";
- sha256 = "00xqlyl3lffc5l0viin1nyp819wf81fncqyz87jx8ljjdhilmgbs";
- };
- }];
-
- fish.shellInit =
- ''
- # nix
- if test -e /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
- fenv source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
- end
-
- # home-manager
- if test -e $HOME/.nix-profile/etc/profile.d/hm-session-vars.sh
- fenv source $HOME/.nix-profile/etc/profile.d/hm-session-vars.sh
- end
- '';
-
- # programs.bash.enable = true;
- bash.initExtra = ''
- function msh() { mosh "$@" -- bash -c 'if type -f tmux; then tmux new-session -A -s 0; else screen -R; fi;' ; };
- '';
-
- # Let Home Manager install and manage itself.
- home-manager.enable = true;
- starship.enable = true;
- starship.settings =
- {
- container.format = "[\\[$name\\]]($style) ";
- git_status = {
- ahead = "⇡$\{count}";
- diverged = "⇕⇡$\{ahead_count}⇣$\{behind_count}";
- behind = "⇣$\{count}";
- };
- };
-
- neovim-flake = {
- enable = true;
- # your settings need to go into the settings attrset
- settings = {
- vim = {
- viAlias = false;
- vimAlias = false;
- debugMode = {
- enable = false;
- level = 20;
- logFile = "/tmp/nvim.log";
- };
- };
-
- vim.lsp = {
- formatOnSave = true;
- lspkind.enable = false;
- lightbulb.enable = true;
- lspsaga.enable = false;
- nvimCodeActionMenu.enable = true;
- trouble.enable = true;
- lspSignature.enable = true;
- lsplines.enable = true;
- };
-
- vim.debugger = {
- nvim-dap = {
- enable = true;
- ui.enable = true;
- };
- };
-
- vim.languages = {
- enableLSP = true;
- enableFormat = true;
- enableTreesitter = true;
- enableExtraDiagnostics = true;
-
- nix = {
- enable = true;
- format.type = "nixpkgs-fmt";
- };
- html.enable = true;
- clang = {
- enable = true;
- lsp.server = "clangd";
- };
- sql.enable = false;
- rust = {
- enable = true;
- crates.enable = true;
- };
- java.enable = true;
- ts.enable = true;
- svelte.enable = true;
- go.enable = true;
- zig.enable = true;
- python.enable = true;
- dart.enable = true;
- elixir.enable = false;
- };
-
- vim.visuals = {
- enable = true;
- nvimWebDevicons.enable = true;
- scrollBar.enable = true;
- smoothScroll.enable = true;
- cellularAutomaton.enable = true;
- fidget-nvim.enable = true;
- indentBlankline = {
- enable = true;
- fillChar = null;
- eolChar = null;
- showCurrContext = true;
- };
- cursorline = {
- enable = true;
- lineTimeout = 0;
- };
- };
-
- vim.statusline = {
- lualine = {
- enable = true;
- theme = "catppuccin";
- };
- };
-
- vim.theme = {
- enable = true;
- name = "catppuccin";
- style = "mocha";
- transparent = false;
- };
-
- vim.autopairs.enable = true;
-
- vim.autocomplete = {
- enable = true;
- type = "nvim-cmp";
- };
-
- vim.filetree = {
- nvimTree = {
- enable = true;
- };
- };
-
- vim.tabline = {
- nvimBufferline.enable = true;
- };
-
- vim.treesitter.context.enable = true;
-
- vim.binds = {
- whichKey.enable = true;
- cheatsheet.enable = true;
- };
-
- vim.telescope.enable = true;
-
- vim.git = {
- enable = true;
- gitsigns.enable = true;
- gitsigns.codeActions = false; # throws an annoying debug message
- };
-
- vim.minimap = {
- minimap-vim.enable = false;
- codewindow.enable = true; # lighter, faster, and uses lua for configuration
- };
-
- vim.dashboard = {
- dashboard-nvim.enable = false;
- alpha.enable = true;
- };
-
- vim.notify = {
- nvim-notify.enable = true;
- };
-
- vim.projects = {
- project-nvim.enable = true;
- };
-
- vim.utility = {
- ccc.enable = true;
- vim-wakatime.enable = false;
- icon-picker.enable = true;
- surround.enable = true;
- diffview-nvim.enable = true;
- motion = {
- hop.enable = true;
- leap.enable = true;
- };
- };
-
- vim.notes = {
- obsidian.enable = false; # FIXME neovim fails to build if obsidian is enabled
- orgmode.enable = false;
- mind-nvim.enable = true;
- todo-comments.enable = true;
- };
-
- vim.terminal = {
- toggleterm = {
- enable = true;
- lazygit.enable = true;
- };
- };
-
- vim.ui = {
- borders.enable = true;
- noice.enable = true;
- colorizer.enable = true;
- modes-nvim.enable = false; # the theme looks terrible with catppuccin
- illuminate.enable = true;
- breadcrumbs = {
- enable = true;
- navbuddy.enable = true;
- };
- smartcolumn = {
- enable = true;
- columnAt.languages = {
- # this is a freeform module, it's `buftype = int;` for configuring column position
- nix = 110;
- ruby = 120;
- java = 130;
- go = [ 90 130 ];
- };
- };
- };
-
- vim.assistant = {
- copilot = {
- enable = true;
- cmp.enable = true;
- };
- };
-
- vim.session = {
- nvim-session-manager.enable = false;
- };
-
- vim.gestures = {
- gesture-nvim.enable = false;
- };
-
- vim.comments = {
- comment-nvim.enable = true;
- };
-
- vim.spellChecking.languages = [ "en" "de" ];
- };
- };
- };
}
diff --git a/lib/audio/default.nix b/lib/audio/default.nix
new file mode 100644
index 0000000..ebe8154
--- /dev/null
+++ b/lib/audio/default.nix
@@ -0,0 +1,65 @@
+{ lib }:
+
+rec {
+ ## Renames an alsa device from a given `name` using the new `description`.
+ ##
+ #@ { name: String, description: String } -> { matches: List, apply_properties: Attrs }
+ mkAlsaRename = { name, description }: {
+ matches = [
+ [
+ [ "device.name" "matches" name ]
+ ]
+ ];
+ # actions = { "update-props" = { "node.description" = description; }; };
+ apply_properties = {
+ "device.description" = description;
+ };
+ };
+
+ ## Create a pipewire audio node.
+ ##
+ #@ { name: String, factory: String ? "adapter", ... } -> { factory: String, args: Attrs }
+ mkAudioNode = args@{ name, factory ? "adapter", ... }: {
+ inherit factory;
+ args = (builtins.removeAttrs args [ "name" "description" ]) // {
+ "node.name" = name;
+ "node.description" = args.description or args."node.description";
+ "factory.name" = args."factory.name" or "support.null-audio-sink";
+ };
+ };
+
+ ## Create a virtual pipewire audio node.
+ ##
+ #@ { name: String, ... } -> { factory: "adapter", args: Attrs }
+ mkVirtualAudioNode = args@{ name, ... }:
+ mkAudioNode (args // {
+ name = "virtual-${lib.toLower name}-audio";
+ description = "${name} (Virtual)";
+ "media.class" = args.class or args."media.class" or "Audio/Duplex";
+ "object.linger" = args."object.linger" or true;
+ "audio.position" = args."audio.position" or [ "FL" "FR" ];
+ "monitor.channel-volumes" = args."monitor.channel-volumes" or true;
+ });
+
+ ## Connect two pipewire audio nodes
+ ##
+ #@ { name: String?, from: String, to: String, ... } -> { name: "libpipewire-module-loopback", args: Attrs }
+ mkBridgeAudioModule = args@{ from, to, ... }: {
+ name = "libpipewire-module-loopback";
+ args = (builtins.removeAttrs args [ "from" "to" "name" ]) // {
+ "node.name" =
+ if args ? name then
+ "${args.name}-bridge"
+ else
+ "${lib.toLower from}-to-${lib.toLower to}-bridge";
+ "audio.position" = args."audio.position" or [ "FL" "FR" ];
+ "capture.props" = {
+ "node.target" = from;
+ } // (args."capture.props" or { });
+ "playback.props" = {
+ "node.target" = to;
+ "monitor.channel-volumes" = true;
+ } // (args."playback.props" or { });
+ };
+ };
+}
diff --git a/lib/default.nix b/lib/default.nix
new file mode 100644
index 0000000..61216de
--- /dev/null
+++ b/lib/default.nix
@@ -0,0 +1,20 @@
+{ lib, inputs, snowfall-inputs }:
+
+rec {
+ ## Override a package's metadata
+ ##
+ ## ```nix
+ ## let
+ ## new-meta = {
+ ## description = "My new description";
+ ## };
+ ## in
+ ## lib.override-meta new-meta pkgs.hello
+ ## ```
+ ##
+ #@ Attrs -> Package -> Package
+ override-meta = meta: package:
+ package.overrideAttrs (attrs: {
+ meta = (attrs.meta or { }) // meta;
+ });
+}
diff --git a/lib/deploy/default.nix b/lib/deploy/default.nix
new file mode 100644
index 0000000..478d7d1
--- /dev/null
+++ b/lib/deploy/default.nix
@@ -0,0 +1,51 @@
+{ lib, inputs }:
+
+let
+ inherit (inputs) deploy-rs;
+in
+rec {
+ ## Create deployment configuration for use with deploy-rs.
+ ##
+ ## ```nix
+ ## mkDeploy {
+ ## inherit self;
+ ## overrides = {
+ ## my-host.system.sudo = "doas -u";
+ ## };
+ ## }
+ ## ```
+ ##
+ #@ { self: Flake, overrides: Attrs ? {} } -> Attrs
+ mkDeploy = { self, overrides ? { } }:
+ let
+ hosts = self.nixosConfigurations or { };
+ names = builtins.attrNames hosts;
+ nodes = lib.foldl
+ (result: name:
+ let
+ host = hosts.${name};
+ user = host.config.plusultra.user.name or null;
+ inherit (host.pkgs) system;
+ in
+ result // {
+ ${name} = (overrides.${name} or { }) // {
+ hostname = overrides.${name}.hostname or "${name}";
+ profiles = (overrides.${name}.profiles or { }) // {
+ system = (overrides.${name}.profiles.system or { }) // {
+ path = deploy-rs.lib.${system}.activate.nixos host;
+ } // lib.optionalAttrs (user != null) {
+ user = "root";
+ sshUser = user;
+ } // lib.optionalAttrs
+ (host.config.plusultra.security.doas.enable or false)
+ {
+ sudo = "doas -u";
+ };
+ };
+ };
+ })
+ { }
+ names;
+ in
+ { inherit nodes; };
+}
diff --git a/lib/file/default.nix b/lib/file/default.nix
new file mode 100644
index 0000000..97a3078
--- /dev/null
+++ b/lib/file/default.nix
@@ -0,0 +1,25 @@
+{ lib, ... }:
+
+rec {
+ ## Append text to the contents of a file
+ ##
+ ## ```nix
+ ## fileWithText ./some.txt "appended text"
+ ## ```
+ ##
+ #@ Path -> String -> String
+ fileWithText = file: text: ''
+ ${builtins.readFile file}
+ ${text}'';
+
+ ## Prepend text to the contents of a file
+ ##
+ ## ```nix
+ ## fileWithText' ./some.txt "prepended text"
+ ## ```
+ ##
+ #@ Path -> String -> String
+ fileWithText' = file: text: ''
+ ${text}
+ ${builtins.readFile file}'';
+}
diff --git a/lib/module/default.nix b/lib/module/default.nix
new file mode 100644
index 0000000..48e53c6
--- /dev/null
+++ b/lib/module/default.nix
@@ -0,0 +1,62 @@
+{ lib, ... }:
+
+with lib; rec {
+ ## Create a NixOS module option.
+ ##
+ ## ```nix
+ ## lib.mkOpt nixpkgs.lib.types.str "My default" "Description of my option."
+ ## ```
+ ##
+ #@ Type -> Any -> String
+ mkOpt = type: default: description:
+ mkOption { inherit type default description; };
+
+ ## Create a NixOS module option without a description.
+ ##
+ ## ```nix
+ ## lib.mkOpt' nixpkgs.lib.types.str "My default"
+ ## ```
+ ##
+ #@ Type -> Any -> String
+ mkOpt' = type: default: mkOpt type default null;
+
+ ## Create a boolean NixOS module option.
+ ##
+ ## ```nix
+ ## lib.mkBoolOpt true "Description of my option."
+ ## ```
+ ##
+ #@ Type -> Any -> String
+ mkBoolOpt = mkOpt types.bool;
+
+ ## Create a boolean NixOS module option without a description.
+ ##
+ ## ```nix
+ ## lib.mkBoolOpt true
+ ## ```
+ ##
+ #@ Type -> Any -> String
+ mkBoolOpt' = mkOpt' types.bool;
+
+ enabled = {
+ ## Quickly enable an option.
+ ##
+ ## ```nix
+ ## services.nginx = enabled;
+ ## ```
+ ##
+ #@ true
+ enable = true;
+ };
+
+ disabled = {
+ ## Quickly disable an option.
+ ##
+ ## ```nix
+ ## services.nginx = enabled;
+ ## ```
+ ##
+ #@ false
+ enable = false;
+ };
+}
diff --git a/lib/network/default.nix b/lib/network/default.nix
new file mode 100644
index 0000000..5b1033a
--- /dev/null
+++ b/lib/network/default.nix
@@ -0,0 +1,54 @@
+{ lib, inputs, snowfall-inputs }:
+
+let
+ inherit (inputs.nixpkgs.lib) assertMsg last;
+in
+{
+ network = {
+ # Split an address to get its host name or ip and its port.
+ # Type: String -> Attrs
+ # Usage: get-address-parts "bismuth:3000"
+ # result: { host = "bismuth"; port = "3000"; }
+ get-address-parts = address:
+ let
+ address-parts = builtins.split ":" address;
+ ip = builtins.head address-parts;
+ host = if ip == "" then "127.0.0.1" else ip;
+ port = if builtins.length address-parts != 3 then "" else last address-parts;
+ in
+ { inherit host port; };
+
+ ## Create proxy configuration for NGINX virtual hosts.
+ ##
+ ## ```nix
+ ## services.nginx.virtualHosts."example.com" = lib.network.create-proxy {
+ ## port = 3000;
+ ## host = "0.0.0.0";
+ ## proxy-web-sockets = true;
+ ## extra-config = {
+ ## forceSSL = true;
+ ## };
+ ## }
+ ## ``
+ ##
+ #@ { port: Int ? null, host: String ? "127.0.0.1", proxy-web-sockets: Bool ? false, extra-config: Attrs ? { } } -> Attrs
+ create-proxy =
+ { port ? null
+ , host ? "127.0.0.1"
+ , proxy-web-sockets ? false
+ , extra-config ? { }
+ }:
+ assert assertMsg (port != "" && port != null) "port cannot be empty";
+ assert assertMsg (host != "") "host cannot be empty";
+ extra-config // {
+ locations = (extra-config.locations or { }) // {
+ "/" = (extra-config.locations."/" or { }) // {
+ proxyPass =
+ "http://${host}${if port != null then ":${builtins.toString port}" else ""}";
+
+ proxyWebsockets = proxy-web-sockets;
+ };
+ };
+ };
+ };
+}
diff --git a/modules/darwin/apps/firefox/default.nix b/modules/darwin/apps/firefox/default.nix
new file mode 100644
index 0000000..61d3487
--- /dev/null
+++ b/modules/darwin/apps/firefox/default.nix
@@ -0,0 +1,58 @@
+{ options
+, config
+, lib
+, pkgs
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.apps.firefox;
+ defaultSettings = {
+ "browser.aboutwelcome.enabled" = false;
+ "browser.meta_refresh_when_inactive.disabled" = true;
+ "browser.startup.homepage" = "https://start.duckduckgo.com/?kak=-1&kal=-1&kao=-1&kaq=-1&kt=Hack+Nerd+Font&kae=d&ks=m&k7=2e3440&kj=3b4252&k9=eceff4&kaa=d8dee9&ku=1&k8=d8dee9&kx=81a1c1&k21=3b4252&k18=1&k5=2&kp=-2&k1=-1&kaj=u&kay=b&kk=-1&kax=-1&kap=-1&kau=-1";
+ "browser.bookmarks.showMobileBookmarks" = true;
+ "browser.urlbar.suggest.quicksuggest.sponsored" = false;
+ "browser.newtabpage.activity-stream.showSponsoredTopSites" = false;
+ "browser.aboutConfig.showWarning" = false;
+ "browser.ssb.enabled" = true;
+ };
+in
+{
+ options.plusultra.apps.firefox = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Firefox.";
+ extraConfig =
+ mkOpt str "" "Extra configuration for the user profile JS file.";
+ userChrome =
+ mkOpt str "" "Extra configuration for the user chrome CSS file.";
+ settings = mkOpt attrs defaultSettings "Settings to apply to the profile.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra.desktop.addons.firefox-nordic-theme = enabled;
+
+ plusultra.home = {
+ extraOptions = {
+ programs.firefox = {
+ enable = true;
+ package = pkgs.firefox.override {
+ cfg = {
+ # FIXME: This method of enabling browser pass support is
+ # deprecated, but the suggested solution requires using
+ # the NixOS module rather than Home-Manager. Update this
+ # if/when Home-Manager gets support for:
+ # `programs.firefox.extraNativeMessagingHosts.packages`
+ enableBrowserpass = true;
+ };
+ };
+
+ profiles.${config.plusultra.user.name} = {
+ inherit (cfg) extraConfig userChrome settings;
+ id = 0;
+ name = config.plusultra.user.name;
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/modules/darwin/apps/iterm2/default.nix b/modules/darwin/apps/iterm2/default.nix
new file mode 100644
index 0000000..5c6178c
--- /dev/null
+++ b/modules/darwin/apps/iterm2/default.nix
@@ -0,0 +1,18 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.iterm2;
+in
+{
+ options.plusultra.apps.iterm2 = with types; {
+ enable = mkBoolOpt false "Whether or not to enable iTerm2.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [
+ iterm2
+ ];
+ };
+}
diff --git a/modules/darwin/apps/vscode/default.nix b/modules/darwin/apps/vscode/default.nix
new file mode 100644
index 0000000..60d0d5b
--- /dev/null
+++ b/modules/darwin/apps/vscode/default.nix
@@ -0,0 +1,14 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.apps.vscode;
+in
+{
+ options.plusultra.apps.vscode = with types; {
+ enable = mkBoolOpt false "Whether or not to enable vscode.";
+ };
+
+ config =
+ mkIf cfg.enable { environment.systemPackages = with pkgs; [ vscode ]; };
+}
diff --git a/modules/darwin/cli-apps/neovim/default.nix b/modules/darwin/cli-apps/neovim/default.nix
new file mode 100644
index 0000000..be46ce9
--- /dev/null
+++ b/modules/darwin/cli-apps/neovim/default.nix
@@ -0,0 +1,47 @@
+inputs @ { options
+, config
+, lib
+, pkgs
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.cli-apps.neovim;
+in
+{
+ options.plusultra.cli-apps.neovim = with types; {
+ enable = mkBoolOpt false "Whether or not to enable neovim.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [
+ # FIXME: As of today (2022-12-09), `page` no longer works with my Neovim
+ # configuration. Either something in my configuration is breaking it or `page` is busted.
+ # page
+ plusultra.neovim
+ ];
+
+ environment.variables = {
+ # PAGER = "page";
+ # MANPAGER =
+ # "page -C -e 'au User PageDisconnect sleep 100m|%y p|enew! |bd! #|pu p|set ft=man'";
+ PAGER = "less";
+ MANPAGER = "less";
+ NPM_CONFIG_PREFIX = "$HOME/.npm-global";
+ EDITOR = "nvim";
+ };
+
+ plusultra.home = {
+ configFile = {
+ "dashboard-nvim/.keep".text = "";
+ };
+
+ extraOptions = {
+ # Use Neovim for Git diffs.
+ programs.zsh.shellAliases.vimdiff = "nvim -d";
+ programs.bash.shellAliases.vimdiff = "nvim -d";
+ programs.fish.shellAliases.vimdiff = "nvim -d";
+ };
+ };
+ };
+}
diff --git a/modules/darwin/desktop/addons/firefox-nordic-theme/default.nix b/modules/darwin/desktop/addons/firefox-nordic-theme/default.nix
new file mode 100644
index 0000000..86033fe
--- /dev/null
+++ b/modules/darwin/desktop/addons/firefox-nordic-theme/default.nix
@@ -0,0 +1,23 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.desktop.addons.firefox-nordic-theme;
+ profileDir = ".mozilla/firefox/${config.plusultra.user.name}";
+in
+{
+ options.plusultra.desktop.addons.firefox-nordic-theme = with types; {
+ enable = mkBoolOpt false "Whether to enable the Nordic theme for firefox.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra.apps.firefox = {
+ extraConfig = builtins.readFile
+ "${pkgs.plusultra.firefox-nordic-theme}/configuration/user.js";
+ userChrome = ''
+ @import "${pkgs.plusultra.firefox-nordic-theme}/userChrome.css";
+ '';
+ };
+ };
+}
diff --git a/modules/darwin/desktop/addons/skhd/default.nix b/modules/darwin/desktop/addons/skhd/default.nix
new file mode 100644
index 0000000..5aac882
--- /dev/null
+++ b/modules/darwin/desktop/addons/skhd/default.nix
@@ -0,0 +1,109 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.desktop.addons.skhd;
+
+ mkScript = name: file: pkgs.writeShellApplication {
+ inherit name;
+ checkPhase = "";
+ text = builtins.readFile file;
+ };
+
+ open-iterm2 = mkScript "open-iterm2" ./scripts/open-iterm2.sh;
+in
+{
+ options.plusultra.desktop.addons.skhd = {
+ enable = mkEnableOption "skhd";
+ };
+
+ config = mkIf cfg.enable {
+ services.skhd = {
+ enable = true;
+
+ skhdConfig = ''
+ # Movement
+ shift + cmd - h : yabai -m window --focus west
+ shift + cmd - j : yabai -m window --focus south
+ shift + cmd - k : yabai -m window --focus north
+ shift + cmd - l : yabai -m window --focus east
+
+ # Window Movement
+ lctrl + shift + cmd - h : yabai -m window --swap west
+ lctrl + shift + cmd - j : yabai -m window --swap south
+ lctrl + shift + cmd - k : yabai -m window --swap north
+ lctrl + shift + cmd - l : yabai -m window --swap east
+
+ # Window Resize
+ lctrl + cmd - h : yabai -m window --resize left:-50:0; \
+ yabai -m window --resize right:-50:0
+ lctrl + cmd - j : yabai -m window --resize bottom:0:50; \
+ yabai -m window --resize top:0:50
+ lctrl + cmd - k : yabai -m window --resize top:0:-50; \
+ yabai -m window --resize bottom:0:-50
+ lctrl + cmd - l : yabai -m window --resize right:50:0; \
+ yabai -m window --resize left:50:0
+
+ lctrl + alt - h : yabai -m window --resize left:-10:0; \
+ yabai -m window --resize right:-10:0
+ lctrl + alt - j : yabai -m window --resize bottom:0:10; \
+ yabai -m window --resize top:0:10
+ lctrl + alt - k : yabai -m window --resize top:0:-10; \
+ yabai -m window --resize bottom:0:-10
+ lctrl + alt - l : yabai -m window --resize right:10:0; \
+ yabai -m window --resize left:10:0
+
+ lctrl + cmd - e : yabai -m space --balance
+
+ # Move Window To Space
+ lctrl + shift + cmd - m : yabai -m window --space last; yabai -m space --focus last
+ lctrl + shift + cmd - p : yabai -m window --space prev; yabai -m space --focus prev
+ lctrl + shift + cmd - n : yabai -m window --space next; yabai -m space --focus next
+ lctrl + shift + cmd - 1 : yabai -m window --space 1; yabai -m space --focus 1
+ lctrl + shift + cmd - 2 : yabai -m window --space 2; yabai -m space --focus 2
+ lctrl + shift + cmd - 3 : yabai -m window --space 3; yabai -m space --focus 3
+ lctrl + shift + cmd - 4 : yabai -m window --space 4; yabai -m space --focus 4
+
+ # Focus Space
+ # shift + cmd - m : yabai -m space --focus last
+ # shift + cmd - p : yabai -m space --focus prev
+ # shift + cmd - n : yabai -m space --focus next
+ shift + cmd - 1 : yabai -m space --focus 1
+ shift + cmd - 2 : yabai -m space --focus 2
+ shift + cmd - 3 : yabai -m space --focus 3
+ shift + cmd - 4 : yabai -m space --focus 4
+
+ # Insert Direction
+ lctrl + shift + cmd - v : yabai -m window --insert south
+ lctrl + shift + cmd - b : yabai -m window --insert east
+ lctrl + shift + cmd - s : yabai -m window --insert stack
+
+ # Floating Windows
+ # shift + cmd - space : \
+ # yabai -m window --toggle float; \
+ # yabai -m window --toggle border
+ shift + cmd - space : yabai -m window --toggle float
+
+ # Terminal
+ shift + cmd - return : ${open-iterm2}/bin/open-iterm2
+
+ # Fullscreen
+ alt - f : yabai -m window --toggle zoom-fullscreen
+ shift + alt - f : yabai -m window --toggle native-fullscreen
+
+ # Restart Yabai
+ shift + lctrl + alt - r : \
+ /usr/bin/env osascript <<< \
+ "display notification \"Restarting Yabai\" with title \"Yabai\""; \
+ launchctl kickstart -k "gui/$UID/org.nixos.yabai"
+
+ # Restart Spacebar
+ shift + lctrl + alt - s : \
+ /usr/bin/env osascript <<< \
+ "display notification \"Restarting Spacebar\" with title \"Spacebar\""; \
+ launchctl kickstart -k "gui/$UID/org.nixos.spacebar"
+ '';
+ };
+ };
+}
diff --git a/modules/darwin/desktop/addons/skhd/scripts/open-iterm2.sh b/modules/darwin/desktop/addons/skhd/scripts/open-iterm2.sh
new file mode 100644
index 0000000..c3d35a4
--- /dev/null
+++ b/modules/darwin/desktop/addons/skhd/scripts/open-iterm2.sh
@@ -0,0 +1,14 @@
+# Detects if iTerm2 is running
+if ! pgrep -f "iTerm" > /dev/null 2>&1; then
+ open -a "/Applications/Nix Apps/iTerm2.app"
+else
+ # Create a new window
+ script='tell application "iTerm2" to create window with default profile'
+ ! osascript -e "${script}" > /dev/null 2>&1 && {
+ # Get pids for any app with "iTerm" and kill
+ while IFS="" read -r pid; do
+ kill -15 "${pid}"
+ done < <(pgrep -f "iTerm")
+ open -a "/Applications/Nix Apps/iTerm2.app"
+ }
+fi
diff --git a/modules/darwin/desktop/addons/spacebar/default.nix b/modules/darwin/desktop/addons/spacebar/default.nix
new file mode 100644
index 0000000..69393cc
--- /dev/null
+++ b/modules/darwin/desktop/addons/spacebar/default.nix
@@ -0,0 +1,47 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.desktop.addons.spacebar;
+in
+{
+ options.plusultra.desktop.addons.spacebar = {
+ enable = mkEnableOption "Spacebar";
+ };
+
+ config = mkIf cfg.enable {
+ services.spacebar = {
+ enable = true;
+ package = pkgs.spacebar;
+
+ config = {
+ position = "top";
+ display = "all";
+ height = 32;
+ title = "on";
+ spaces = "on";
+ clock = "on";
+ power = "off";
+
+ padding_left = 10;
+ padding_right = 10;
+
+ spacing_left = 10;
+ spacing_right = 10;
+
+ foreground_color = "0xffeceff4";
+ background_color = "0xff1d2128";
+
+ text_font = ''"Hack Nerd Font Mono:Regular:14.0"'';
+ icon_font = ''"Hack Nerd Font Mono:Regular:20.0"'';
+
+ # Shell entries apparently break the whole bar...
+ # https://github.com/cmacrae/spacebar/issues/104
+ # right_shell_icon = "";
+ # right_shell_command = ''"whoami"'';
+ # right_shell = "on";
+ };
+ };
+ };
+}
diff --git a/modules/darwin/desktop/yabai/default.nix b/modules/darwin/desktop/yabai/default.nix
new file mode 100644
index 0000000..1404d8c
--- /dev/null
+++ b/modules/darwin/desktop/yabai/default.nix
@@ -0,0 +1,77 @@
+{ lib
+, pkgs
+, config
+, ...
+}:
+let
+ cfg = config.plusultra.desktop.yabai;
+
+ inherit (lib) types mkEnableOption mkIf;
+ inherit (lib.plusultra) mkOpt enabled;
+in
+{
+ options.plusultra.desktop.yabai = {
+ enable = mkEnableOption "Yabai";
+ enable-scripting-addition = mkOpt types.bool true "Whether to enable the scripting addition for Yabai. (Requires SIP to be disabled)";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra.desktop.addons = {
+ skhd = enabled;
+ spacebar = enabled;
+ };
+
+ services.yabai = {
+ enable = true;
+ enableScriptingAddition = cfg.enable-scripting-addition;
+
+ config = {
+ layout = "bsp";
+
+ auto_balance = "off";
+
+ debug_output = "on";
+
+ top_padding = 8;
+ right_padding = 8;
+ left_padding = 8;
+ bottom_padding = 8;
+
+ window_gap = 6;
+ window_topmost = "on";
+ window_shadow = "float";
+
+ # As of macOS Sonoma, window borders break Yabai and cause a bunch of lag.
+ window_border = "off";
+ # window_border = "on";
+ # window_border_width = 5;
+ # window_border_radius = 14;
+ # window_border_blur = "off";
+ # window_border_hidpi = "on";
+ # insert_feedback_color = "0xffb48ead";
+ # normal_window_border_color = "0x002e3440";
+ # active_window_border_color = "0xff5e81ac";
+
+ external_bar = "all:${builtins.toString config.services.spacebar.config.height}:0";
+
+ # mouse_modifier = "alt";
+ mouse_modifier = "cmd";
+ mouse_action1 = "move";
+ mouse_action2 = "resize";
+ };
+
+ extraConfig = ''
+ yabai -m rule --add label="Finder" app="^Finder$" title="(Co(py|nnect)|Move|Info|Pref)" manage=off
+ yabai -m rule --add label="Safari" app="^Safari$" title="^(General|(Tab|Password|Website|Extension)s|AutoFill|Se(arch|curity)|Privacy|Advance)$" manage=off
+ yabai -m rule --add label="System Preferences" app="^System Preferences$" title=".*" manage=off
+ yabai -m rule --add label="App Store" app="^App Store$" manage=off
+ yabai -m rule --add label="Activity Monitor" app="^Activity Monitor$" manage=off
+ yabai -m rule --add label="Calculator" app="^Calculator$" manage=off
+ yabai -m rule --add label="Dictionary" app="^Dictionary$" manage=off
+ yabai -m rule --add label="mpv" app="^mpv$" manage=off
+ yabai -m rule --add label="Software Update" title="Software Update" manage=off
+ yabai -m rule --add label="About This Mac" app="System Information" title="About This Mac" manage=off
+ '';
+ };
+ };
+}
diff --git a/modules/darwin/home/default.nix b/modules/darwin/home/default.nix
new file mode 100644
index 0000000..01d7157
--- /dev/null
+++ b/modules/darwin/home/default.nix
@@ -0,0 +1,40 @@
+{ options, config, pkgs, lib, inputs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.home;
+in
+{
+ # imports = with inputs; [
+ # home-manager.darwinModules.home-manager
+ # ];
+
+ options.plusultra.home = with types; {
+ file = mkOpt attrs { }
+ "A set of files to be managed by home-manager's .";
+ configFile = mkOpt attrs { }
+ "A set of files to be managed by home-manager's .";
+ extraOptions = mkOpt attrs { } "Options to pass directly to home-manager.";
+ homeConfig = mkOpt attrs { } "Final config for home-manager.";
+ };
+
+ config = {
+ plusultra.home.extraOptions = {
+ home.stateVersion = mkDefault "22.11";
+ home.file = mkAliasDefinitions options.plusultra.home.file;
+ xdg.enable = true;
+ xdg.configFile = mkAliasDefinitions options.plusultra.home.configFile;
+ };
+
+ snowfallorg.user.${config.plusultra.user.name}.home.config = mkAliasDefinitions options.plusultra.home.extraOptions;
+
+ home-manager = {
+ useUserPackages = true;
+ useGlobalPkgs = true;
+
+ # users.${config.plusultra.user.name} = args:
+ # mkAliasDefinitions options.plusultra.home.extraOptions;
+ };
+ };
+}
diff --git a/modules/darwin/nix/default.nix b/modules/darwin/nix/default.nix
new file mode 100644
index 0000000..995aebc
--- /dev/null
+++ b/modules/darwin/nix/default.nix
@@ -0,0 +1,74 @@
+{ options
+, config
+, pkgs
+, lib
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.nix;
+in
+{
+ options.plusultra.nix = with types; {
+ enable = mkBoolOpt true "Whether or not to manage nix configuration.";
+ package = mkOpt package pkgs.nixUnstable "Which nix package to use.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [
+ deploy-rs
+ nixfmt
+ nix-index
+ nix-prefetch-git
+ ];
+
+ nix =
+ let
+ users = [ "root" config.plusultra.user.name ];
+ in
+ {
+ package = cfg.package;
+
+ settings = {
+ experimental-features = "nix-command flakes";
+ http-connections = 50;
+ warn-dirty = false;
+ log-lines = 50;
+
+ # Large builds apparently fail due to an issue with darwin:
+ # https://github.com/NixOS/nix/issues/4119
+ sandbox = false;
+
+ # This appears to break on darwin
+ # https://github.com/NixOS/nix/issues/7273
+ auto-optimise-store = false;
+
+ allow-import-from-derivation = true;
+
+ trusted-users = users;
+ allowed-users = users;
+
+ # NOTE: This configuration is generated by nix-installer so I'm adding it here in
+ # case it becomes important.
+ extra-nix-path = "nixpkgs=flake:nixpkgs";
+ build-users-group = "nixbld";
+ };
+ #// (lib.optionalAttrs config.plusultra.tools.direnv.enable {
+ # keep-outputs = true;
+ # keep-derivations = true;
+ #});
+
+ gc = {
+ automatic = true;
+ interval = { Day = 7; };
+ options = "--delete-older-than 30d";
+ user = config.plusultra.user.name;
+ };
+
+ # flake-utils-plus
+ generateRegistryFromInputs = true;
+ generateNixPathFromInputs = true;
+ linkInputs = true;
+ };
+ };
+}
diff --git a/modules/darwin/security/gpg/default.nix b/modules/darwin/security/gpg/default.nix
new file mode 100644
index 0000000..80a4961
--- /dev/null
+++ b/modules/darwin/security/gpg/default.nix
@@ -0,0 +1,84 @@
+{ lib, config, pkgs, inputs, ... }:
+
+let
+ inherit (lib) types mkEnableOption mkIf;
+ inherit (lib.plusultra) mkOpt;
+
+ cfg = config.plusultra.security.gpg;
+
+ gpgConf = "${inputs.gpg-base-conf}/gpg.conf";
+
+ gpgAgentConf = ''
+ enable-ssh-support
+ default-cache-ttl 60
+ max-cache-ttl 120
+ '';
+
+ guide = "${inputs.yubikey-guide}/README.md";
+
+ theme = pkgs.fetchFromGitHub {
+ owner = "jez";
+ repo = "pandoc-markdown-css-theme";
+ rev = "019a4829242937761949274916022e9861ed0627";
+ sha256 = "1h48yqffpaz437f3c9hfryf23r95rr319lrb3y79kxpxbc9hihxb";
+ };
+
+ guideHTML = pkgs.runCommand "yubikey-guide" { } ''
+ ${pkgs.pandoc}/bin/pandoc \
+ --standalone \
+ --metadata title="Yubikey Guide" \
+ --from markdown \
+ --to html5+smart \
+ --toc \
+ --template ${theme}/template.html5 \
+ --css ${theme}/docs/css/theme.css \
+ --css ${theme}/docs/css/skylighting-solarized-theme.css \
+ -o $out \
+ ${guide}
+ '';
+
+ reload-yubikey = pkgs.writeShellScriptBin "reload-yubikey" ''
+ ${pkgs.gnupg}/bin/gpg-connect-agent "scd serialno" "learn --force" /bye
+ '';
+in
+{
+ options.plusultra.security.gpg = {
+ enable = mkEnableOption "GPG";
+ agentTimeout = mkOpt types.int 5 "The amount of time to wait before continuing with shell init.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [
+ gnupg
+ ];
+
+ environment.shellInit = ''
+ export GPG_TTY="$(tty)"
+ export SSH_AUTH_SOCK=$(${pkgs.gnupg}/bin/gpgconf --list-dirs agent-ssh-socket)
+
+ ${pkgs.coreutils}/bin/timeout ${builtins.toString cfg.agentTimeout} ${pkgs.gnupg}/bin/gpgconf --launch gpg-agent
+ gpg_agent_timeout_status=$?
+
+ if [ "$gpg_agent_timeout_status" = 124 ]; then
+ # Command timed out...
+ echo "GPG Agent timed out..."
+ echo 'Run "gpgconf --launch gpg-agent" to try and launch it again.'
+ fi
+ '';
+
+ programs.gnupg.agent = {
+ enable = true;
+ enableSSHSupport = true;
+ };
+
+ plusultra.home.file = {
+ ".gnupg/.keep".text = "";
+
+ ".gnupg/yubikey-guide.md".source = guide;
+ ".gnupg/yubikey-guide.html".source = guideHTML;
+
+ ".gnupg/gpg.conf".source = gpgConf;
+ ".gnupg/gpg-agent.conf".text = gpgAgentConf;
+ };
+ };
+}
diff --git a/modules/darwin/services/nix-daemon/default.nix b/modules/darwin/services/nix-daemon/default.nix
new file mode 100644
index 0000000..020ea6d
--- /dev/null
+++ b/modules/darwin/services/nix-daemon/default.nix
@@ -0,0 +1,17 @@
+{ lib, config, ... }:
+
+let
+ inherit (lib) types mkIf;
+ inherit (lib.plusultra) mkOpt enabled;
+
+ cfg = config.plusultra.services.nix-daemon;
+in
+{
+ options.plusultra.services.nix-daemon = {
+ enable = mkOpt types.bool true "Whether to enable the Nix daemon.";
+ };
+
+ config = mkIf cfg.enable {
+ services.nix-daemon = enabled;
+ };
+}
diff --git a/modules/darwin/suites/common/default.nix b/modules/darwin/suites/common/default.nix
new file mode 100644
index 0000000..485971f
--- /dev/null
+++ b/modules/darwin/suites/common/default.nix
@@ -0,0 +1,43 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.suites.common;
+in
+{
+ options.plusultra.suites.common = with types; {
+ enable = mkBoolOpt false "Whether or not to enable common configuration.";
+ };
+
+ config = mkIf cfg.enable {
+ programs.zsh = enabled;
+
+ plusultra = {
+ nix = enabled;
+
+ apps = {
+ iterm2 = enabled;
+ };
+
+ cli-apps = {
+ neovim = enabled;
+ };
+
+ tools = {
+ git = enabled;
+ flake = enabled;
+ };
+
+ system = {
+ fonts = enabled;
+ input = enabled;
+ interface = enabled;
+ };
+
+ security = {
+ gpg = enabled;
+ };
+ };
+ };
+}
diff --git a/modules/darwin/suites/development/default.nix b/modules/darwin/suites/development/default.nix
new file mode 100644
index 0000000..cafd387
--- /dev/null
+++ b/modules/darwin/suites/development/default.nix
@@ -0,0 +1,35 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.suites.development;
+in
+{
+ options.plusultra.suites.development = with types; {
+ enable = mkBoolOpt false
+ "Whether or not to enable common development configuration.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra = {
+ apps = {
+ vscode = enabled;
+ };
+
+ tools = {
+ # at = enabled;
+ # direnv = enabled;
+ # go = enabled;
+ # http = enabled;
+ # k8s = enabled;
+ node = enabled;
+ # titan = enabled;
+ python = enabled;
+ java = enabled;
+ };
+
+ # virtualisation = { podman = enabled; };
+ };
+ };
+}
diff --git a/modules/darwin/system/fonts/default.nix b/modules/darwin/system/fonts/default.nix
new file mode 100644
index 0000000..b1483e0
--- /dev/null
+++ b/modules/darwin/system/fonts/default.nix
@@ -0,0 +1,32 @@
+{ options, config, pkgs, lib, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.system.fonts;
+in
+{
+ options.plusultra.system.fonts = with types; {
+ enable = mkBoolOpt false "Whether or not to manage fonts.";
+ fonts = mkOpt (listOf package) [ ] "Custom font packages to install.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.variables = {
+ # Enable icons in tooling since we have nerdfonts.
+ LOG_ICONS = "true";
+ };
+
+ fonts = {
+ fontDir = enabled;
+
+ fonts = with pkgs;
+ [
+ noto-fonts
+ noto-fonts-cjk-sans
+ noto-fonts-cjk-serif
+ noto-fonts-emoji
+ (nerdfonts.override { fonts = [ "Hack" ]; })
+ ] ++ cfg.fonts;
+ };
+ };
+}
diff --git a/modules/darwin/system/input/DefaultKeyBinding.dict b/modules/darwin/system/input/DefaultKeyBinding.dict
new file mode 100644
index 0000000..b65cc38
--- /dev/null
+++ b/modules/darwin/system/input/DefaultKeyBinding.dict
@@ -0,0 +1 @@
+{ "~a" = (); "~b" = (); "~c" = (); "~d" = (); "~e" = (); "~f" = (); "~g" = (); "~h" = (); "~i" = (); "~j" = (); "~k" = (); "~l" = (); "~m" = (); "~n" = (); "~o" = (); "~p" = (); "~q" = (); "~r" = (); "~s" = (); "~t" = (); "~u" = (); "~v" = (); "~w" = (); "~x" = (); "~y" = (); "~z" = (); }
diff --git a/modules/darwin/system/input/default.nix b/modules/darwin/system/input/default.nix
new file mode 100644
index 0000000..dfcbec5
--- /dev/null
+++ b/modules/darwin/system/input/default.nix
@@ -0,0 +1,55 @@
+{ options, config, pkgs, lib, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.system.input;
+in
+{
+ options.plusultra.system.input = with types; {
+ enable = mkEnableOption "macOS input";
+ };
+
+ config = mkIf cfg.enable (mkMerge [
+ {
+ system = {
+ keyboard = {
+ enableKeyMapping = true;
+ remapCapsLockToEscape = true;
+ };
+
+ defaults = {
+ ".GlobalPreferences" = {
+ "com.apple.mouse.scaling" = "1";
+ };
+
+ NSGlobalDomain = {
+ AppleKeyboardUIMode = 3;
+ ApplePressAndHoldEnabled = false;
+
+ KeyRepeat = 2;
+ InitialKeyRepeat = 15;
+
+ NSAutomaticCapitalizationEnabled = false;
+ NSAutomaticDashSubstitutionEnabled = false;
+ NSAutomaticQuoteSubstitutionEnabled = false;
+ NSAutomaticPeriodSubstitutionEnabled = false;
+ NSAutomaticSpellingCorrectionEnabled = false;
+ };
+ };
+ };
+
+ snowfallorg.user.${config.plusultra.user.name}.home.config = {
+ home.activation = {
+ # Disable special keys when using Option as a modifier.
+ # https://superuser.com/questions/941286/disable-default-option-key-binding
+ disableSpecialKeys = lib.home-manager.hm.dag.entryAfter [ "writeBoundary" ] ''
+ set +e
+ $DRY_RUN_CMD /usr/bin/sudo mkdir -p $HOME/Library/KeyBindings
+ $DRY_RUN_CMD /usr/bin/sudo cp '${builtins.toPath ./DefaultKeyBinding.dict}' "$HOME/Library/KeyBindings/DefaultKeyBinding.dict"
+ set -e
+ '';
+ };
+ };
+ }
+ ]);
+}
diff --git a/modules/darwin/system/interface/default.nix b/modules/darwin/system/interface/default.nix
new file mode 100644
index 0000000..4ecfdf5
--- /dev/null
+++ b/modules/darwin/system/interface/default.nix
@@ -0,0 +1,29 @@
+{ options, config, pkgs, lib, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.system.interface;
+in
+{
+ options.plusultra.system.interface = with types; {
+ enable = mkEnableOption "macOS interface";
+ };
+
+ config = mkIf cfg.enable {
+ system.defaults = {
+ dock.autohide = true;
+
+ finder = {
+ AppleShowAllExtensions = true;
+ FXEnableExtensionChangeWarning = false;
+ };
+
+ NSGlobalDomain = {
+ _HIHideMenuBar = true;
+ AppleShowScrollBars = "Always";
+ };
+ };
+
+ plusultra.home.file.".hushlogin".text = "";
+ };
+}
diff --git a/modules/darwin/tools/flake/default.nix b/modules/darwin/tools/flake/default.nix
new file mode 100644
index 0000000..9f50aee
--- /dev/null
+++ b/modules/darwin/tools/flake/default.nix
@@ -0,0 +1,18 @@
+{ lib, config, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.tools.flake;
+in
+{
+ options.plusultra.tools.flake = {
+ enable = mkEnableOption "Flake";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [
+ snowfallorg.flake
+ ];
+ };
+}
diff --git a/modules/darwin/tools/git/default.nix b/modules/darwin/tools/git/default.nix
new file mode 100644
index 0000000..c13ba67
--- /dev/null
+++ b/modules/darwin/tools/git/default.nix
@@ -0,0 +1,43 @@
+{ options, config, pkgs, lib, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.tools.git;
+ gpg = config.plusultra.security.gpg;
+ user = config.plusultra.user;
+in
+{
+ options.plusultra.tools.git = with types; {
+ enable = mkBoolOpt false "Whether or not to install and configure git.";
+ userName = mkOpt types.str user.fullName "The name to configure git with.";
+ userEmail = mkOpt types.str user.email "The email to configure git with.";
+ signingKey =
+ mkOpt types.str "9762169A1B35EA68" "The key ID to sign commits with.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [ git ];
+
+ plusultra.home.extraOptions = {
+ programs.git = {
+ enable = true;
+ inherit (cfg) userName userEmail;
+ lfs = enabled;
+ signing = {
+ key = cfg.signingKey;
+ signByDefault = mkIf gpg.enable true;
+ };
+ extraConfig = {
+ init = { defaultBranch = "main"; };
+ pull = { rebase = true; };
+ push = { autoSetupRemote = true; };
+ core = { whitespace = "trailing-space,space-before-tab"; };
+ safe = {
+ directory = "${config.users.users.${user.name}.home}/work/config";
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/modules/darwin/tools/java/default.nix b/modules/darwin/tools/java/default.nix
new file mode 100644
index 0000000..aa51095
--- /dev/null
+++ b/modules/darwin/tools/java/default.nix
@@ -0,0 +1,17 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.tools.java;
+in
+{
+ options.plusultra.tools.java = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Java.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [
+ jdk
+ ];
+ };
+}
diff --git a/modules/darwin/tools/node/default.nix b/modules/darwin/tools/node/default.nix
new file mode 100644
index 0000000..9a70afd
--- /dev/null
+++ b/modules/darwin/tools/node/default.nix
@@ -0,0 +1,42 @@
+{ options
+, config
+, pkgs
+, lib
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.tools.node;
+in
+{
+ options.plusultra.tools.node = with types; {
+ enable = mkBoolOpt false "Whether or not to install and configure git";
+ pkg = mkOpt package pkgs.nodejs "The NodeJS package to use";
+ prettier = {
+ enable = mkBoolOpt true "Whether or not to install Prettier";
+ pkg =
+ mkOpt package pkgs.nodePackages.prettier "The NodeJS package to use";
+ };
+ yarn = {
+ enable = mkBoolOpt true "Whether or not to install Yarn";
+ pkg = mkOpt package pkgs.nodePackages.yarn "The NodeJS package to use";
+ };
+ pnpm = {
+ enable = mkBoolOpt true "Whether or not to install Pnpm";
+ pkg = mkOpt package pkgs.nodePackages.pnpm "The NodeJS package to use";
+ };
+ flyctl = {
+ enable = mkBoolOpt true "Whether or not to install flyctl";
+ pkg = mkOpt package pkgs.flyctl "The flyctl package to use";
+ };
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs;
+ [ cfg.pkg ]
+ ++ (lib.optional cfg.prettier.enable cfg.prettier.pkg)
+ ++ (lib.optional cfg.yarn.enable cfg.yarn.pkg)
+ ++ (lib.optional cfg.pnpm.enable cfg.pnpm.pkg)
+ ++ (lib.optional cfg.flyctl.enable cfg.flyctl.pkg);
+ };
+}
diff --git a/modules/darwin/tools/python/default.nix b/modules/darwin/tools/python/default.nix
new file mode 100644
index 0000000..34de5a8
--- /dev/null
+++ b/modules/darwin/tools/python/default.nix
@@ -0,0 +1,22 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.tools.python;
+in
+{
+ options.plusultra.tools.python = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Python.";
+ };
+
+ config =
+ mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [
+ (python311.withPackages (ps:
+ with ps; [
+ numpy
+ ])
+ )
+ ];
+ };
+}
diff --git a/modules/darwin/user/default.nix b/modules/darwin/user/default.nix
new file mode 100644
index 0000000..ccea8d3
--- /dev/null
+++ b/modules/darwin/user/default.nix
@@ -0,0 +1,45 @@
+{ lib
+, config
+, pkgs
+, ...
+}:
+let
+ inherit (lib) types mkIf mkDefault;
+ inherit (lib.plusultra) mkOpt;
+
+ cfg = config.plusultra.user;
+
+ is-linux = pkgs.stdenv.isLinux;
+ is-darwin = pkgs.stdenv.isDarwin;
+in
+{
+ options.plusultra.user = {
+ name = mkOpt types.str "short" "The user account.";
+
+ fullName = mkOpt types.str "Jake Hamilton" "The full name of the user.";
+ email = mkOpt types.str "jake.hamilton@hey.com" "The email of the user.";
+
+ uid = mkOpt (types.nullOr types.int) 501 "The uid for the user account.";
+ };
+
+ config = {
+ users.users.${cfg.name} = {
+ # NOTE: Setting the uid here is required for another
+ # module to evaluate successfully since it reads
+ # `users.users.${plusultra.user.name}.uid`.
+ uid = mkIf (cfg.uid != null) cfg.uid;
+ };
+
+ snowfallorg.user.${config.plusultra.user.name}.home.config = {
+ home = {
+ file = {
+ ".profile".text = ''
+ # The default file limit is far too low and throws an error when rebuilding the system.
+ # See the original with: ulimit -Sa
+ ulimit -n 4096
+ '';
+ };
+ };
+ };
+ };
+}
diff --git a/modules/home/cli-apps/bash/default.nix b/modules/home/cli-apps/bash/default.nix
new file mode 100644
index 0000000..d6d316e
--- /dev/null
+++ b/modules/home/cli-apps/bash/default.nix
@@ -0,0 +1,27 @@
+{ lib
+, config
+, pkgs
+, ...
+}:
+let
+ inherit (lib) mkEnableOption mkIf;
+
+ cfg = config.plusultra.cli-apps.bash;
+in
+{
+ options.plusultra.cli-apps.bash = {
+ enable = mkEnableOption "BASH shell";
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = with pkgs; [
+ bashInteractive
+ ];
+ programs.bash = {
+ enable = true;
+ initExtra = ''
+ function msh() { mosh "$@" -- bash -c 'if type -f tmux; then tmux new-session -A -s 0; else screen -R; fi;' ; };
+ '';
+ };
+ };
+}
diff --git a/modules/home/cli-apps/bat/default.nix b/modules/home/cli-apps/bat/default.nix
new file mode 100644
index 0000000..4eadb9c
--- /dev/null
+++ b/modules/home/cli-apps/bat/default.nix
@@ -0,0 +1,22 @@
+{ lib
+, config
+, pkgs
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.cli-apps.bat;
+in
+{
+ options.plusultra.cli-apps.bat = {
+ enable = mkEnableOption "bat";
+ };
+
+ config = mkIf cfg.enable {
+ programs.bat = {
+ enable = true;
+ config.theme = "ansi";
+ extraPackages = with pkgs.bat-extras; [ batdiff batman batgrep batwatch ];
+ };
+ };
+}
diff --git a/modules/home/cli-apps/fish/default.nix b/modules/home/cli-apps/fish/default.nix
new file mode 100644
index 0000000..5318b32
--- /dev/null
+++ b/modules/home/cli-apps/fish/default.nix
@@ -0,0 +1,53 @@
+{ lib
+, config
+, pkgs
+, ...
+}:
+let
+ inherit (lib) mkEnableOption mkIf;
+
+ cfg = config.plusultra.cli-apps.fish;
+in
+{
+ options.plusultra.cli-apps.fish = {
+ enable = mkEnableOption "FISH shell";
+ };
+
+ config = mkIf cfg.enable {
+ programs.fish = {
+ enable = true;
+ interactiveShellInit = ''
+ function msh --wraps mosh --description 'mosh with tmux'
+ if not set -q argv[1]
+ echo 'Usage: msh [user@]host [command]'
+ else
+ ${pkgs.mosh}/bin/mosh $argv -- tmux new-session -A -s 0
+ end
+ end
+ '';
+
+ plugins = [{
+ name = "foreign-env";
+ src = pkgs.fetchFromGitHub {
+ owner = "oh-my-fish";
+ repo = "plugin-foreign-env";
+ rev = "dddd9213272a0ab848d474d0cbde12ad034e65bc";
+ sha256 = "00xqlyl3lffc5l0viin1nyp819wf81fncqyz87jx8ljjdhilmgbs";
+ };
+ }];
+
+ shellInit =
+ ''
+ # nix
+ if test -e /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
+ fenv source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
+ end
+
+ # home-manager
+ if test -e $HOME/.nix-profile/etc/profile.d/hm-session-vars.sh
+ fenv source $HOME/.nix-profile/etc/profile.d/hm-session-vars.sh
+ end
+ '';
+ };
+ };
+}
diff --git a/modules/home/cli-apps/git/default.nix b/modules/home/cli-apps/git/default.nix
new file mode 100644
index 0000000..f2c2c0b
--- /dev/null
+++ b/modules/home/cli-apps/git/default.nix
@@ -0,0 +1,49 @@
+{ lib
+, config
+, pkgs
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.cli-apps.git;
+in
+{
+ options.plusultra.cli-apps.git = {
+ enable = mkEnableOption "git";
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = with pkgs; [
+ delta
+ gh
+ ];
+ programs.git = {
+ enable = true;
+ userName = "Harald Hoyer";
+ userEmail = "harald@hoyer.xyz";
+ extraConfig = {
+ init.defaultBranch = "main";
+ "credential \"https://github.com\"" = {
+ helper = "!gh auth git-credential";
+ };
+ alias = {
+ co = "checkout";
+ ci = "commit";
+ };
+ pull.ff = "only";
+ core.pager = "delta";
+ delta = {
+ features = "side-by-side line-numbers decorations";
+ syntax-theme = "DarkNeon";
+ light = "false";
+ line-numbers = "true";
+ navigate = "true";
+ };
+ interactive.diffFilter = "${pkgs.delta}/bin/delta --color-only";
+ merge.conflictStyle = "diff3";
+ diff.colorMoved = "default";
+ };
+ };
+
+ };
+}
diff --git a/modules/home/cli-apps/home-manager/default.nix b/modules/home/cli-apps/home-manager/default.nix
new file mode 100644
index 0000000..450c53c
--- /dev/null
+++ b/modules/home/cli-apps/home-manager/default.nix
@@ -0,0 +1,33 @@
+{ lib, config, pkgs, ... }:
+
+let
+ inherit (lib) mkEnableOption mkIf;
+ inherit (lib.plusultra) enabled;
+
+ cfg = config.plusultra.cli-apps.home-manager;
+in
+{
+ options.plusultra.cli-apps.home-manager = {
+ enable = mkEnableOption "home-manager";
+ };
+
+ config = mkIf cfg.enable {
+ programs.home-manager = enabled;
+ home.sessionVariables = {
+ EDITOR = "${pkgs.vim}/bin/vim";
+ BATDIFF_USE_DELTA = "true";
+ };
+
+ home.shellAliases = {
+ cat = "${pkgs.bat}/bin/bat --decorations never";
+ less = ''${pkgs.bat}/bin/bat --decorations never --paging=always --pager "${pkgs.less}/bin/less -RF"'';
+ man = "${pkgs.bat-extras.batman}/bin/batman";
+ };
+
+ home.packages = with pkgs; [
+ bat
+ vim
+ cachix
+ ];
+ };
+}
diff --git a/modules/home/cli-apps/neovim/default.nix b/modules/home/cli-apps/neovim/default.nix
new file mode 100644
index 0000000..1b08bcb
--- /dev/null
+++ b/modules/home/cli-apps/neovim/default.nix
@@ -0,0 +1,230 @@
+{ lib, config, pkgs, ... }:
+
+let
+ inherit (lib) mkEnableOption mkIf;
+
+ cfg = config.plusultra.cli-apps.neovim;
+in
+{
+ options.plusultra.cli-apps.neovim = {
+ enable = mkEnableOption "Neovim";
+ };
+
+ config = mkIf cfg.enable {
+ programs.neovim-flake = {
+ enable = true;
+ # your settings need to go into the settings attrset
+ settings = {
+ vim = {
+ viAlias = false;
+ vimAlias = false;
+ debugMode = {
+ enable = false;
+ level = 20;
+ logFile = "/tmp/nvim.log";
+ };
+ };
+
+ vim.lsp = {
+ formatOnSave = true;
+ lspkind.enable = false;
+ lightbulb.enable = true;
+ lspsaga.enable = false;
+ nvimCodeActionMenu.enable = true;
+ trouble.enable = true;
+ lspSignature.enable = true;
+ lsplines.enable = true;
+ };
+
+ vim.debugger = {
+ nvim-dap = {
+ enable = true;
+ ui.enable = true;
+ };
+ };
+
+ vim.languages = {
+ enableLSP = true;
+ enableFormat = true;
+ enableTreesitter = true;
+ enableExtraDiagnostics = true;
+
+ nix = {
+ enable = true;
+ format.type = "nixpkgs-fmt";
+ };
+ html.enable = true;
+ clang = {
+ enable = true;
+ lsp.server = "clangd";
+ };
+ sql.enable = false;
+ rust = {
+ enable = true;
+ crates.enable = true;
+ };
+ java.enable = true;
+ ts.enable = true;
+ svelte.enable = true;
+ go.enable = true;
+ zig.enable = true;
+ python.enable = true;
+ dart.enable = true;
+ elixir.enable = false;
+ };
+
+ vim.visuals = {
+ enable = true;
+ nvimWebDevicons.enable = true;
+ scrollBar.enable = true;
+ smoothScroll.enable = true;
+ cellularAutomaton.enable = true;
+ fidget-nvim.enable = true;
+ indentBlankline = {
+ enable = true;
+ fillChar = null;
+ eolChar = null;
+ showCurrContext = true;
+ };
+ cursorline = {
+ enable = true;
+ lineTimeout = 0;
+ };
+ };
+
+ vim.statusline = {
+ lualine = {
+ enable = true;
+ theme = "catppuccin";
+ };
+ };
+
+ vim.theme = {
+ enable = true;
+ name = "catppuccin";
+ style = "mocha";
+ transparent = false;
+ };
+
+ vim.autopairs.enable = true;
+
+ vim.autocomplete = {
+ enable = true;
+ type = "nvim-cmp";
+ };
+
+ vim.filetree = {
+ nvimTree = {
+ enable = true;
+ };
+ };
+
+ vim.tabline = {
+ nvimBufferline.enable = true;
+ };
+
+ vim.treesitter.context.enable = true;
+
+ vim.binds = {
+ whichKey.enable = true;
+ cheatsheet.enable = true;
+ };
+
+ vim.telescope.enable = true;
+
+ vim.git = {
+ enable = true;
+ gitsigns.enable = true;
+ gitsigns.codeActions = false; # throws an annoying debug message
+ };
+
+ vim.minimap = {
+ minimap-vim.enable = false;
+ codewindow.enable = true; # lighter, faster, and uses lua for configuration
+ };
+
+ vim.dashboard = {
+ dashboard-nvim.enable = false;
+ alpha.enable = true;
+ };
+
+ vim.notify = {
+ nvim-notify.enable = true;
+ };
+
+ vim.projects = {
+ project-nvim.enable = true;
+ };
+
+ vim.utility = {
+ ccc.enable = true;
+ vim-wakatime.enable = false;
+ icon-picker.enable = true;
+ surround.enable = true;
+ diffview-nvim.enable = true;
+ motion = {
+ hop.enable = true;
+ leap.enable = true;
+ };
+ };
+
+ vim.notes = {
+ obsidian.enable = false; # FIXME neovim fails to build if obsidian is enabled
+ orgmode.enable = false;
+ mind-nvim.enable = true;
+ todo-comments.enable = true;
+ };
+
+ vim.terminal = {
+ toggleterm = {
+ enable = true;
+ lazygit.enable = true;
+ };
+ };
+
+ vim.ui = {
+ borders.enable = true;
+ noice.enable = true;
+ colorizer.enable = true;
+ modes-nvim.enable = false; # the theme looks terrible with catppuccin
+ illuminate.enable = true;
+ breadcrumbs = {
+ enable = true;
+ navbuddy.enable = true;
+ };
+ smartcolumn = {
+ enable = true;
+ columnAt.languages = {
+ # this is a freeform module, it's `buftype = int;` for configuring column position
+ nix = 110;
+ ruby = 120;
+ java = 130;
+ go = [ 90 130 ];
+ };
+ };
+ };
+
+ vim.assistant = {
+ copilot = {
+ enable = true;
+ cmp.enable = true;
+ };
+ };
+
+ vim.session = {
+ nvim-session-manager.enable = false;
+ };
+
+ vim.gestures = {
+ gesture-nvim.enable = false;
+ };
+
+ vim.comments = {
+ comment-nvim.enable = true;
+ };
+
+ vim.spellChecking.languages = [ "en" "de" ];
+ };
+ };
+ };
+}
diff --git a/modules/home/cli-apps/starship/default.nix b/modules/home/cli-apps/starship/default.nix
new file mode 100644
index 0000000..82bc343
--- /dev/null
+++ b/modules/home/cli-apps/starship/default.nix
@@ -0,0 +1,32 @@
+{ lib
+, config
+, pkgs
+, ...
+}:
+let
+ inherit (lib) mkEnableOption mkIf;
+
+ cfg = config.plusultra.cli-apps.starship;
+in
+{
+ options.plusultra.cli-apps.starship = {
+ enable = mkEnableOption "starship";
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = with pkgs; [
+ (pkgs.nerdfonts.override { fonts = [ "FiraCode" "DroidSansMono" "JetBrainsMono" ]; })
+ ];
+ programs.starship = {
+ enable = true;
+ settings = {
+ container.format = "[\\[$name\\]]($style) ";
+ git_status = {
+ ahead = "⇡$\{count}";
+ diverged = "⇕⇡$\{ahead_count}⇣$\{behind_count}";
+ behind = "⇣$\{count}";
+ };
+ };
+ };
+ };
+}
diff --git a/modules/home/cli-apps/tmux/default.nix b/modules/home/cli-apps/tmux/default.nix
new file mode 100644
index 0000000..89218d7
--- /dev/null
+++ b/modules/home/cli-apps/tmux/default.nix
@@ -0,0 +1,20 @@
+{ lib
+, config
+, pkgs
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.cli-apps.tmux;
+in
+{
+ options.plusultra.cli-apps.tmux = {
+ enable = mkEnableOption "Tmux";
+ };
+
+ config = mkIf cfg.enable {
+ home.packages = with pkgs; [
+ tmux
+ ];
+ };
+}
diff --git a/modules/home/host/default.nix b/modules/home/host/default.nix
new file mode 100644
index 0000000..9c22b68
--- /dev/null
+++ b/modules/home/host/default.nix
@@ -0,0 +1,11 @@
+{ lib, config, pkgs, host ? null, format ? "unknown", ... }:
+
+let
+ inherit (lib) types;
+ inherit (lib.plusultra) mkOpt;
+in
+{
+ options.plusultra.host = {
+ name = mkOpt (types.nullOr types.str) host "The host name.";
+ };
+}
diff --git a/modules/home/tools/direnv/default.nix b/modules/home/tools/direnv/default.nix
new file mode 100644
index 0000000..f9a77c7
--- /dev/null
+++ b/modules/home/tools/direnv/default.nix
@@ -0,0 +1,18 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.tools.direnv;
+in
+{
+ options.plusultra.tools.direnv = with types; {
+ enable = mkBoolOpt false "Whether or not to enable direnv.";
+ };
+
+ config = mkIf cfg.enable {
+ programs.direnv = {
+ enable = true;
+ nix-direnv = enabled;
+ };
+ };
+}
diff --git a/modules/home/tools/git/default.nix b/modules/home/tools/git/default.nix
new file mode 100644
index 0000000..4392d50
--- /dev/null
+++ b/modules/home/tools/git/default.nix
@@ -0,0 +1,40 @@
+{ lib, config, pkgs, ... }:
+
+let
+ inherit (lib) types mkEnableOption mkIf;
+ inherit (lib.plusultra) mkOpt enabled;
+
+ cfg = config.plusultra.tools.git;
+ user = config.plusultra.user;
+in
+{
+ options.plusultra.tools.git = {
+ enable = mkEnableOption "Git";
+ userName = mkOpt types.str user.fullName "The name to configure git with.";
+ userEmail = mkOpt types.str user.email "The email to configure git with.";
+ signingKey =
+ mkOpt types.str "9762169A1B35EA68" "The key ID to sign commits with.";
+ signByDefault = mkOpt types.bool true "Whether to sign commits by default.";
+ };
+
+ config = mkIf cfg.enable {
+ programs.git = {
+ enable = true;
+ inherit (cfg) userName userEmail;
+ lfs = enabled;
+ signing = {
+ key = cfg.signingKey;
+ inherit (cfg) signByDefault;
+ };
+ extraConfig = {
+ init = { defaultBranch = "main"; };
+ pull = { rebase = true; };
+ push = { autoSetupRemote = true; };
+ core = { whitespace = "trailing-space,space-before-tab"; };
+ safe = {
+ directory = "${user.home}/work/config";
+ };
+ };
+ };
+ };
+}
diff --git a/modules/home/tools/jetbrains/default.nix b/modules/home/tools/jetbrains/default.nix
new file mode 100644
index 0000000..03159d2
--- /dev/null
+++ b/modules/home/tools/jetbrains/default.nix
@@ -0,0 +1,21 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.tools.jetbrains;
+in
+{
+ options.plusultra.tools.jetbrains = with types; {
+ enable = mkBoolOpt false "Whether or not to enable jetbrains.";
+ };
+
+ config = mkIf cfg.enable {
+ home.sessionPath = [
+ "$HOME/.local/share/JetBrains/Toolbox/scripts"
+ ];
+ home.packages = with pkgs; [
+ jetbrains-toolbox
+ (pkgs.nerdfonts.override { fonts = [ "FiraCode" "DroidSansMono" "JetBrainsMono" ]; })
+ ];
+ };
+}
diff --git a/modules/home/tools/ssh/default.nix b/modules/home/tools/ssh/default.nix
new file mode 100644
index 0000000..6d6cd55
--- /dev/null
+++ b/modules/home/tools/ssh/default.nix
@@ -0,0 +1,20 @@
+{ lib, config, pkgs, ... }:
+
+let
+ inherit (lib) types mkEnableOption mkIf;
+ cfg = config.plusultra.tools.ssh;
+in
+{
+ options.plusultra.tools.ssh = {
+ enable = mkEnableOption "SSH";
+ };
+
+ config = mkIf cfg.enable {
+ programs.ssh = {
+ extraConfig = ''
+ Host *
+ HostKeyAlgorithms +ssh-rsa
+ '';
+ };
+ };
+}
diff --git a/modules/home/user/default.nix b/modules/home/user/default.nix
new file mode 100644
index 0000000..2489926
--- /dev/null
+++ b/modules/home/user/default.nix
@@ -0,0 +1,50 @@
+{ lib, config, pkgs, osConfig ? { }, ... }:
+
+let
+ inherit (lib) types mkIf mkDefault mkMerge;
+ inherit (lib.plusultra) mkOpt;
+
+ cfg = config.plusultra.user;
+
+ is-linux = pkgs.stdenv.isLinux;
+ is-darwin = pkgs.stdenv.isDarwin;
+
+ home-directory =
+ if cfg.name == null then
+ null
+ else if is-darwin then
+ "/Users/${cfg.name}"
+ else
+ "/home/${cfg.name}";
+in
+{
+ options.plusultra.user = {
+ enable = mkOpt types.bool false "Whether to configure the user account.";
+ name = mkOpt (types.nullOr types.str) config.snowfallorg.user.name "The user account.";
+
+ fullName = mkOpt types.str "Jake Hamilton" "The full name of the user.";
+ email = mkOpt types.str "jake.hamilton@hey.com" "The email of the user.";
+
+ home = mkOpt (types.nullOr types.str) home-directory "The user's home directory.";
+ };
+
+ config = mkIf cfg.enable (mkMerge [
+ {
+ assertions = [
+ {
+ assertion = cfg.name != null;
+ message = "plusultra.user.name must be set";
+ }
+ {
+ assertion = cfg.home != null;
+ message = "plusultra.user.home must be set";
+ }
+ ];
+
+ home = {
+ username = mkDefault cfg.name;
+ homeDirectory = mkDefault cfg.home;
+ };
+ }
+ ]);
+}
diff --git a/modules/nixos/apps/_1password/default.nix b/modules/nixos/apps/_1password/default.nix
new file mode 100644
index 0000000..74eda46
--- /dev/null
+++ b/modules/nixos/apps/_1password/default.nix
@@ -0,0 +1,23 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps._1password;
+in
+{
+ options.plusultra.apps._1password = with types; {
+ enable = mkBoolOpt false "Whether or not to enable 1password.";
+ };
+
+ config = mkIf cfg.enable {
+ programs = {
+ _1password = enabled;
+ _1password-gui = {
+ enable = true;
+
+ polkitPolicyOwners = [ config.plusultra.user.name ];
+ };
+ };
+ };
+}
diff --git a/modules/nixos/apps/ardour/default.nix b/modules/nixos/apps/ardour/default.nix
new file mode 100644
index 0000000..0049768
--- /dev/null
+++ b/modules/nixos/apps/ardour/default.nix
@@ -0,0 +1,15 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.ardour;
+in
+{
+ options.plusultra.apps.ardour = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Ardour.";
+ };
+
+ config =
+ mkIf cfg.enable { environment.systemPackages = with pkgs; [ ardour ]; };
+}
diff --git a/modules/nixos/apps/blender/default.nix b/modules/nixos/apps/blender/default.nix
new file mode 100644
index 0000000..4c8182b
--- /dev/null
+++ b/modules/nixos/apps/blender/default.nix
@@ -0,0 +1,14 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.apps.blender;
+in
+{
+ options.plusultra.apps.blender = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Blender.";
+ };
+
+ config =
+ mkIf cfg.enable { environment.systemPackages = with pkgs; [ blender ]; };
+}
diff --git a/modules/nixos/apps/bottles/default.nix b/modules/nixos/apps/bottles/default.nix
new file mode 100644
index 0000000..b445f27
--- /dev/null
+++ b/modules/nixos/apps/bottles/default.nix
@@ -0,0 +1,14 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.apps.bottles;
+in
+{
+ options.plusultra.apps.bottles = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Bottles.";
+ };
+
+ config =
+ mkIf cfg.enable { environment.systemPackages = with pkgs; [ bottles ]; };
+}
diff --git a/modules/nixos/apps/cadence/default.nix b/modules/nixos/apps/cadence/default.nix
new file mode 100644
index 0000000..ae65bd8
--- /dev/null
+++ b/modules/nixos/apps/cadence/default.nix
@@ -0,0 +1,14 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.apps.cadence;
+in
+{
+ options.plusultra.apps.cadence = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Cadence.";
+ };
+
+ config =
+ mkIf cfg.enable { environment.systemPackages = with pkgs; [ cadence ]; };
+}
diff --git a/modules/nixos/apps/discord/default.nix b/modules/nixos/apps/discord/default.nix
new file mode 100644
index 0000000..fb1d5b8
--- /dev/null
+++ b/modules/nixos/apps/discord/default.nix
@@ -0,0 +1,41 @@
+{ options, config, lib, pkgs, inputs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.discord;
+ discord = lib.replugged.makeDiscordPlugged {
+ inherit pkgs;
+
+ # This is currently broken, but could speed up Discord startup in the future.
+ withOpenAsar = false;
+
+ plugins = {
+ inherit (inputs) discord-tweaks;
+ };
+
+ themes = {
+ inherit (inputs) discord-nord-theme;
+ };
+ };
+in
+{
+ options.plusultra.apps.discord = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Discord.";
+ canary.enable = mkBoolOpt false "Whether or not to enable Discord Canary.";
+ chromium.enable = mkBoolOpt false
+ "Whether or not to enable the Chromium version of Discord.";
+ firefox.enable = mkBoolOpt false
+ "Whether or not to enable the Firefox version of Discord.";
+ native.enable = mkBoolOpt false "Whether or not to enable the native version of Discord.";
+ };
+
+ config = mkIf (cfg.enable or cfg.chromium.enable) {
+ environment.systemPackages =
+ lib.optional cfg.enable discord
+ ++ lib.optional cfg.canary.enable pkgs.plusultra.discord
+ ++ lib.optional cfg.chromium.enable pkgs.plusultra.discord-chromium
+ ++ lib.optional cfg.firefox.enable pkgs.plusultra.discord-firefox
+ ++ lib.optional cfg.native.enable pkgs.discord;
+ };
+}
diff --git a/modules/nixos/apps/dolphin/default.nix b/modules/nixos/apps/dolphin/default.nix
new file mode 100644
index 0000000..7b69215
--- /dev/null
+++ b/modules/nixos/apps/dolphin/default.nix
@@ -0,0 +1,18 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.apps.dolphin;
+in
+{
+ options.plusultra.apps.dolphin = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Dolphin.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [ dolphin-emu ];
+
+ # Enable GameCube controller support.
+ services.udev.packages = [ pkgs.dolphinEmu ];
+ };
+}
diff --git a/modules/nixos/apps/doukutsu-rs/default.nix b/modules/nixos/apps/doukutsu-rs/default.nix
new file mode 100644
index 0000000..af267d2
--- /dev/null
+++ b/modules/nixos/apps/doukutsu-rs/default.nix
@@ -0,0 +1,30 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.doukutsu-rs;
+ desktopItem = pkgs.makeDesktopItem {
+ name = "doukutsu-rs";
+ desktopName = "doukutsu-rs";
+ genericName =
+ "A fully playable re-implementation of Cave Story (Doukutsu Monogatari) engine written in Rust.";
+ exec = "${pkgs.plusultra.doukutsu-rs}/bin/doukutsu-rs";
+ icon = ./icon.png;
+ type = "Application";
+ categories = [ "Game" "AdventureGame" ];
+ terminal = false;
+ };
+in
+{
+ options.plusultra.apps.doukutsu-rs = with types; {
+ enable = mkBoolOpt false "Whether or not to enable doukutsu-rs.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs.plusultra; [
+ doukutsu-rs
+ desktopItem
+ ];
+ };
+}
diff --git a/modules/nixos/apps/doukutsu-rs/icon.png b/modules/nixos/apps/doukutsu-rs/icon.png
new file mode 100644
index 0000000..acf69cc
Binary files /dev/null and b/modules/nixos/apps/doukutsu-rs/icon.png differ
diff --git a/modules/nixos/apps/element/default.nix b/modules/nixos/apps/element/default.nix
new file mode 100644
index 0000000..20c4591
--- /dev/null
+++ b/modules/nixos/apps/element/default.nix
@@ -0,0 +1,16 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.element;
+in
+{
+ options.plusultra.apps.element = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Element.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [ element-desktop ];
+ };
+}
diff --git a/modules/nixos/apps/etcher/default.nix b/modules/nixos/apps/etcher/default.nix
new file mode 100644
index 0000000..561f7fd
--- /dev/null
+++ b/modules/nixos/apps/etcher/default.nix
@@ -0,0 +1,22 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.etcher;
+in
+{
+ options.plusultra.apps.etcher = with types; {
+ enable = mkBoolOpt false "Whether or not to enable etcher.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs;
+ [
+ # Etcher is currently broken in nixpkgs, temporarily replaced with
+ # gnome disk utility.
+ # etcher
+ gnome.gnome-disk-utility
+ ];
+ };
+}
diff --git a/modules/nixos/apps/expressvpn/default.nix b/modules/nixos/apps/expressvpn/default.nix
new file mode 100644
index 0000000..5664833
--- /dev/null
+++ b/modules/nixos/apps/expressvpn/default.nix
@@ -0,0 +1,35 @@
+{ lib, config, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.expressvpn;
+in
+{
+ options.plusultra.apps.expressvpn = {
+ enable = mkEnableOption "Express VPN";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [
+ plusultra.expressvpn
+ ] ++ optionals config.plusultra.desktop.gnome.enable [
+ gnomeExtensions.evpn-shell-assistant
+ ];
+
+ boot.kernelModules = [ "tun" ];
+
+ systemd.services.expressvpn = {
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target" "network-online.target" ];
+
+ description = "ExpressVPN Daemon";
+
+ serviceConfig = {
+ ExecStart = "${pkgs.plusultra.expressvpn}/bin/expressvpnd";
+ Restart = "on-failure";
+ RestartSec = 5;
+ };
+ };
+ };
+}
diff --git a/modules/nixos/apps/firefox/default.nix b/modules/nixos/apps/firefox/default.nix
new file mode 100644
index 0000000..4b592e5
--- /dev/null
+++ b/modules/nixos/apps/firefox/default.nix
@@ -0,0 +1,66 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.firefox;
+ defaultSettings = {
+ "browser.aboutwelcome.enabled" = false;
+ "browser.meta_refresh_when_inactive.disabled" = true;
+ "browser.startup.homepage" = "https://hamho.me";
+ "browser.bookmarks.showMobileBookmarks" = true;
+ "browser.urlbar.suggest.quicksuggest.sponsored" = false;
+ "browser.newtabpage.activity-stream.showSponsoredTopSites" = false;
+ "browser.aboutConfig.showWarning" = false;
+ "browser.ssb.enabled" = true;
+ };
+in
+{
+ options.plusultra.apps.firefox = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Firefox.";
+ extraConfig =
+ mkOpt str "" "Extra configuration for the user profile JS file.";
+ userChrome =
+ mkOpt str "" "Extra configuration for the user chrome CSS file.";
+ settings = mkOpt attrs defaultSettings "Settings to apply to the profile.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra.desktop.addons.firefox-nordic-theme = enabled;
+
+ services.gnome.gnome-browser-connector.enable = config.plusultra.desktop.gnome.enable;
+
+ plusultra.home = {
+ file = {
+ ".mozilla/native-messaging-hosts/com.dannyvankooten.browserpass.json".source = "${pkgs.browserpass}/lib/mozilla/native-messaging-hosts/com.dannyvankooten.browserpass.json";
+
+ ".mozilla/native-messaging-hosts/org.gnome.chrome_gnome_shell.json".source = mkIf config.plusultra.desktop.gnome.enable "${pkgs.chrome-gnome-shell}/lib/mozilla/native-messaging-hosts/org.gnome.chrome_gnome_shell.json";
+ };
+
+ extraOptions = {
+ programs.firefox = {
+ enable = true;
+ package = pkgs.firefox.override (
+ {
+ cfg = {
+ enableBrowserpass = true;
+ enableGnomeExtensions = config.plusultra.desktop.gnome.enable;
+ };
+
+ extraNativeMessagingHosts =
+ optional
+ config.plusultra.desktop.gnome.enable
+ pkgs.gnomeExtensions.gsconnect;
+ }
+ );
+
+ profiles.${config.plusultra.user.name} = {
+ inherit (cfg) extraConfig userChrome settings;
+ id = 0;
+ name = config.plusultra.user.name;
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/modules/nixos/apps/frappe-books/default.nix b/modules/nixos/apps/frappe-books/default.nix
new file mode 100644
index 0000000..9259548
--- /dev/null
+++ b/modules/nixos/apps/frappe-books/default.nix
@@ -0,0 +1,16 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.frappe-books;
+in
+{
+ options.plusultra.apps.frappe-books = with types; {
+ enable = mkBoolOpt false "Whether or not to enable FrappeBooks.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [ plusultra.frappe-books ];
+ };
+}
diff --git a/modules/nixos/apps/freetube/default.nix b/modules/nixos/apps/freetube/default.nix
new file mode 100644
index 0000000..ff8f613
--- /dev/null
+++ b/modules/nixos/apps/freetube/default.nix
@@ -0,0 +1,15 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.freetube;
+in
+{
+ options.plusultra.apps.freetube = with types; {
+ enable = mkBoolOpt false "Whether or not to enable FreeTube.";
+ };
+
+ config =
+ mkIf cfg.enable { environment.systemPackages = with pkgs; [ freetube ]; };
+}
diff --git a/modules/nixos/apps/gimp/default.nix b/modules/nixos/apps/gimp/default.nix
new file mode 100644
index 0000000..4eb46fb
--- /dev/null
+++ b/modules/nixos/apps/gimp/default.nix
@@ -0,0 +1,15 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.gimp;
+in
+{
+ options.plusultra.apps.gimp = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Gimp.";
+ };
+
+ config =
+ mkIf cfg.enable { environment.systemPackages = with pkgs; [ gimp ]; };
+}
diff --git a/modules/nixos/apps/gparted/default.nix b/modules/nixos/apps/gparted/default.nix
new file mode 100644
index 0000000..5ac7eaa
--- /dev/null
+++ b/modules/nixos/apps/gparted/default.nix
@@ -0,0 +1,15 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.gparted;
+in
+{
+ options.plusultra.apps.gparted = with types; {
+ enable = mkBoolOpt false "Whether or not to enable gparted.";
+ };
+
+ config =
+ mkIf cfg.enable { environment.systemPackages = with pkgs; [ gparted ]; };
+}
diff --git a/modules/nixos/apps/hey/default.nix b/modules/nixos/apps/hey/default.nix
new file mode 100644
index 0000000..1e391c5
--- /dev/null
+++ b/modules/nixos/apps/hey/default.nix
@@ -0,0 +1,14 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.hey;
+in
+{
+ options.plusultra.apps.hey = with types; {
+ enable = mkBoolOpt false "Whether or not to enable HEY.";
+ };
+
+ config = mkIf cfg.enable { environment.systemPackages = with pkgs.plusultra; [ hey ]; };
+}
diff --git a/modules/nixos/apps/inkscape/default.nix b/modules/nixos/apps/inkscape/default.nix
new file mode 100644
index 0000000..49886af
--- /dev/null
+++ b/modules/nixos/apps/inkscape/default.nix
@@ -0,0 +1,15 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.apps.inkscape;
+in
+{
+ options.plusultra.apps.inkscape = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Inkscape.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [ inkscape-with-extensions google-fonts ];
+ };
+}
diff --git a/modules/nixos/apps/logseq/default.nix b/modules/nixos/apps/logseq/default.nix
new file mode 100644
index 0000000..9f0eefa
--- /dev/null
+++ b/modules/nixos/apps/logseq/default.nix
@@ -0,0 +1,14 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.apps.logseq;
+in
+{
+ options.plusultra.apps.logseq = with types; {
+ enable = mkBoolOpt false "Whether or not to enable logseq.";
+ };
+
+ config =
+ mkIf cfg.enable { environment.systemPackages = with pkgs; [ logseq ]; };
+}
diff --git a/modules/nixos/apps/looking-glass-client/client.ini b/modules/nixos/apps/looking-glass-client/client.ini
new file mode 100644
index 0000000..01348d1
--- /dev/null
+++ b/modules/nixos/apps/looking-glass-client/client.ini
@@ -0,0 +1,9 @@
+[input]
+escapeKey=56
+rawMouse=yes
+mouseSens=6
+
+[win]
+size=1920x1080
+autoResize=yes
+quickSplash=yes
diff --git a/modules/nixos/apps/looking-glass-client/default.nix b/modules/nixos/apps/looking-glass-client/default.nix
new file mode 100644
index 0000000..a6b329f
--- /dev/null
+++ b/modules/nixos/apps/looking-glass-client/default.nix
@@ -0,0 +1,23 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.looking-glass-client;
+ user = config.plusultra.user;
+in
+{
+ options.plusultra.apps.looking-glass-client = with types; {
+ enable =
+ mkBoolOpt false "Whether or not to enable the Looking Glass client.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [ looking-glass-client ];
+
+ environment.etc."looking-glass-client.ini" = {
+ user = "+${toString config.users.users.${user.name}.uid}";
+ source = ./client.ini;
+ };
+ };
+}
diff --git a/modules/nixos/apps/lutris/default.nix b/modules/nixos/apps/lutris/default.nix
new file mode 100644
index 0000000..8cd088d
--- /dev/null
+++ b/modules/nixos/apps/lutris/default.nix
@@ -0,0 +1,21 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.lutris;
+in
+{
+ options.plusultra.apps.lutris = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Lutris.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [
+ lutris
+ # Needed for some installers like League of Legends
+ openssl
+ gnome.zenity
+ ];
+ };
+}
diff --git a/modules/nixos/apps/obs/default.nix b/modules/nixos/apps/obs/default.nix
new file mode 100644
index 0000000..f0cc035
--- /dev/null
+++ b/modules/nixos/apps/obs/default.nix
@@ -0,0 +1,25 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.obs;
+in
+{
+ options.plusultra.apps.obs = with types; {
+ enable = mkBoolOpt false "Whether or not to enable support for OBS.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = [
+ (pkgs.wrapOBS {
+ plugins = with pkgs.obs-studio-plugins; [
+ wlrobs
+ obs-multi-rtmp
+ obs-move-transition
+ looking-glass-obs
+ ];
+ })
+ ];
+ };
+}
diff --git a/modules/nixos/apps/pcsx2/default.nix b/modules/nixos/apps/pcsx2/default.nix
new file mode 100644
index 0000000..b35d6b9
--- /dev/null
+++ b/modules/nixos/apps/pcsx2/default.nix
@@ -0,0 +1,15 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.pcsx2;
+in
+{
+ options.plusultra.apps.pcsx2 = with types; {
+ enable = mkBoolOpt false "Whether or not to enable PCSX2.";
+ };
+
+ config =
+ mkIf cfg.enable { environment.systemPackages = with pkgs; [ pcsx2 ]; };
+}
diff --git a/modules/nixos/apps/pitivi/default.nix b/modules/nixos/apps/pitivi/default.nix
new file mode 100644
index 0000000..7398ba7
--- /dev/null
+++ b/modules/nixos/apps/pitivi/default.nix
@@ -0,0 +1,15 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.pitivi;
+in
+{
+ options.plusultra.apps.pitivi = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Pitivi.";
+ };
+
+ config =
+ mkIf cfg.enable { environment.systemPackages = with pkgs; [ pitivi ]; };
+}
diff --git a/modules/nixos/apps/pocketcasts/default.nix b/modules/nixos/apps/pocketcasts/default.nix
new file mode 100644
index 0000000..3d2463f
--- /dev/null
+++ b/modules/nixos/apps/pocketcasts/default.nix
@@ -0,0 +1,16 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.pocketcasts;
+in
+{
+ options.plusultra.apps.pocketcasts = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Pocketcasts.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs.plusultra; [ pocketcasts ];
+ };
+}
diff --git a/modules/nixos/apps/prismlauncher/default.nix b/modules/nixos/apps/prismlauncher/default.nix
new file mode 100644
index 0000000..ae692bd
--- /dev/null
+++ b/modules/nixos/apps/prismlauncher/default.nix
@@ -0,0 +1,15 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.prismlauncher;
+in
+{
+ options.plusultra.apps.prismlauncher = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Prism Launcher.";
+ };
+
+ config =
+ mkIf cfg.enable { environment.systemPackages = with pkgs; [ prismlauncher ]; };
+}
diff --git a/modules/nixos/apps/protontricks/default.nix b/modules/nixos/apps/protontricks/default.nix
new file mode 100644
index 0000000..2c2c12d
--- /dev/null
+++ b/modules/nixos/apps/protontricks/default.nix
@@ -0,0 +1,16 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.protontricks;
+in
+{
+ options.plusultra.apps.protontricks = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Protontricks.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [ protontricks ];
+ };
+}
diff --git a/modules/nixos/apps/r2modman/default.nix b/modules/nixos/apps/r2modman/default.nix
new file mode 100644
index 0000000..9dc7190
--- /dev/null
+++ b/modules/nixos/apps/r2modman/default.nix
@@ -0,0 +1,18 @@
+{ options
+, config
+, lib
+, pkgs
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.apps.r2modman;
+in
+{
+ options.plusultra.apps.r2modman = with types; {
+ enable = mkBoolOpt false "Whether or not to enable r2modman.";
+ };
+
+ config =
+ mkIf cfg.enable { environment.systemPackages = with pkgs; [ r2modman ]; };
+}
diff --git a/modules/nixos/apps/rpcs3/default.nix b/modules/nixos/apps/rpcs3/default.nix
new file mode 100644
index 0000000..ed28244
--- /dev/null
+++ b/modules/nixos/apps/rpcs3/default.nix
@@ -0,0 +1,16 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.rpcs3;
+in
+{
+ options.plusultra.apps.rpcs3 = with types; {
+ enable = mkBoolOpt false "Whether or not to enable rpcs3.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [ rpcs3 ];
+ };
+}
diff --git a/modules/nixos/apps/steam/default.nix b/modules/nixos/apps/steam/default.nix
new file mode 100644
index 0000000..3cadadc
--- /dev/null
+++ b/modules/nixos/apps/steam/default.nix
@@ -0,0 +1,30 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.steam;
+in
+{
+ options.plusultra.apps.steam = with types; {
+ enable = mkBoolOpt false "Whether or not to enable support for Steam.";
+ };
+
+ config = mkIf cfg.enable {
+ programs.steam.enable = true;
+ programs.steam.remotePlay.openFirewall = true;
+
+ hardware.steam-hardware.enable = true;
+
+ # Enable GameCube controller support.
+ services.udev.packages = [ pkgs.dolphinEmu ];
+
+ environment.systemPackages = with pkgs.plusultra; [
+ steam
+ ];
+
+ environment.sessionVariables = {
+ STEAM_EXTRA_COMPAT_TOOLS_PATHS = "$HOME/.steam/root/compatibilitytools.d";
+ };
+ };
+}
diff --git a/modules/nixos/apps/steamtinkerlaunch/default.nix b/modules/nixos/apps/steamtinkerlaunch/default.nix
new file mode 100644
index 0000000..39b7577
--- /dev/null
+++ b/modules/nixos/apps/steamtinkerlaunch/default.nix
@@ -0,0 +1,18 @@
+{ lib, pkgs, config, ... }:
+
+let
+ cfg = config.plusultra.apps.steamtinkerlaunch;
+
+ inherit (lib) mkIf mkEnableOption;
+in
+{
+ options.plusultra.apps.steamtinkerlaunch = {
+ enable = mkEnableOption "Steam Tinker Launch";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [
+ steamtinkerlaunch
+ ];
+ };
+}
diff --git a/modules/nixos/apps/twitter/default.nix b/modules/nixos/apps/twitter/default.nix
new file mode 100644
index 0000000..9992996
--- /dev/null
+++ b/modules/nixos/apps/twitter/default.nix
@@ -0,0 +1,15 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.twitter;
+in
+{
+ options.plusultra.apps.twitter = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Twitter.";
+ };
+
+ config =
+ mkIf cfg.enable { environment.systemPackages = with pkgs.plusultra; [ twitter ]; };
+}
diff --git a/modules/nixos/apps/ubports-installer/default.nix b/modules/nixos/apps/ubports-installer/default.nix
new file mode 100644
index 0000000..e000d51
--- /dev/null
+++ b/modules/nixos/apps/ubports-installer/default.nix
@@ -0,0 +1,23 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.ubports-installer;
+in
+{
+ options.plusultra.apps.ubports-installer = with types; {
+ enable = mkBoolOpt false "Whether or not to enable the UBPorts Installer.";
+ };
+
+ config =
+ mkIf cfg.enable {
+ environment.systemPackages = with pkgs.plusultra; [
+ ubports-installer
+ ];
+
+ services.udev.packages = with pkgs.plusultra; [
+ ubports-installer-udev-rules
+ ];
+ };
+}
diff --git a/modules/nixos/apps/virtualbox/default.nix b/modules/nixos/apps/virtualbox/default.nix
new file mode 100644
index 0000000..c04418f
--- /dev/null
+++ b/modules/nixos/apps/virtualbox/default.nix
@@ -0,0 +1,21 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.virtualbox;
+in
+{
+ options.plusultra.apps.virtualbox = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Virtualbox.";
+ };
+
+ config = mkIf cfg.enable {
+ virtualisation.virtualbox.host = {
+ enable = true;
+ enableExtensionPack = true;
+ };
+
+ plusultra.user.extraGroups = [ "vboxusers" ];
+ };
+}
diff --git a/modules/nixos/apps/vlc/default.nix b/modules/nixos/apps/vlc/default.nix
new file mode 100644
index 0000000..d7b13de
--- /dev/null
+++ b/modules/nixos/apps/vlc/default.nix
@@ -0,0 +1,14 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.vlc;
+in
+{
+ options.plusultra.apps.vlc = with types; {
+ enable = mkBoolOpt false "Whether or not to enable vlc.";
+ };
+
+ config = mkIf cfg.enable { environment.systemPackages = with pkgs; [ vlc ]; };
+}
diff --git a/modules/nixos/apps/vscode/default.nix b/modules/nixos/apps/vscode/default.nix
new file mode 100644
index 0000000..60d0d5b
--- /dev/null
+++ b/modules/nixos/apps/vscode/default.nix
@@ -0,0 +1,14 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.apps.vscode;
+in
+{
+ options.plusultra.apps.vscode = with types; {
+ enable = mkBoolOpt false "Whether or not to enable vscode.";
+ };
+
+ config =
+ mkIf cfg.enable { environment.systemPackages = with pkgs; [ vscode ]; };
+}
diff --git a/modules/nixos/apps/winetricks/default.nix b/modules/nixos/apps/winetricks/default.nix
new file mode 100644
index 0000000..1775c14
--- /dev/null
+++ b/modules/nixos/apps/winetricks/default.nix
@@ -0,0 +1,15 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.winetricks;
+in
+{
+ options.plusultra.apps.winetricks = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Winetricks.";
+ };
+
+ config =
+ mkIf cfg.enable { environment.systemPackages = with pkgs; [ winetricks ]; };
+}
diff --git a/modules/nixos/apps/yt-music/default.nix b/modules/nixos/apps/yt-music/default.nix
new file mode 100644
index 0000000..cdd794e
--- /dev/null
+++ b/modules/nixos/apps/yt-music/default.nix
@@ -0,0 +1,15 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.yt-music;
+in
+{
+ options.plusultra.apps.yt-music = with types; {
+ enable = mkBoolOpt false "Whether or not to enable YouTube Music.";
+ };
+
+ config =
+ mkIf cfg.enable { environment.systemPackages = with pkgs.plusultra; [ yt-music ]; };
+}
diff --git a/modules/nixos/apps/yubikey/default.nix b/modules/nixos/apps/yubikey/default.nix
new file mode 100644
index 0000000..23b6b01
--- /dev/null
+++ b/modules/nixos/apps/yubikey/default.nix
@@ -0,0 +1,17 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.yubikey;
+in
+{
+ options.plusultra.apps.yubikey = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Yubikey.";
+ };
+
+ config = mkIf cfg.enable {
+ services.yubikey-agent.enable = true;
+ environment.systemPackages = with pkgs; [ yubikey-manager-qt ];
+ };
+}
diff --git a/modules/nixos/apps/yuzu/default.nix b/modules/nixos/apps/yuzu/default.nix
new file mode 100644
index 0000000..1b495db
--- /dev/null
+++ b/modules/nixos/apps/yuzu/default.nix
@@ -0,0 +1,16 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.apps.yuzu;
+in
+{
+ options.plusultra.apps.yuzu = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Yuzu.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [ yuzu-mainline ];
+ };
+}
diff --git a/modules/nixos/archetypes/gaming/default.nix b/modules/nixos/archetypes/gaming/default.nix
new file mode 100644
index 0000000..42eb950
--- /dev/null
+++ b/modules/nixos/archetypes/gaming/default.nix
@@ -0,0 +1,22 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.archetypes.gaming;
+in
+{
+ options.plusultra.archetypes.gaming = with types; {
+ enable = mkBoolOpt false "Whether or not to enable the gaming archetype.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra.suites = {
+ common = enabled;
+ desktop = enabled;
+ games = enabled;
+ social = enabled;
+ media = enabled;
+ };
+ };
+}
diff --git a/modules/nixos/archetypes/server/default.nix b/modules/nixos/archetypes/server/default.nix
new file mode 100644
index 0000000..6a79a5d
--- /dev/null
+++ b/modules/nixos/archetypes/server/default.nix
@@ -0,0 +1,26 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.archetypes.server;
+in
+{
+ options.plusultra.archetypes.server = with types; {
+ enable =
+ mkBoolOpt false "Whether or not to enable the server archetype.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra = {
+ suites = {
+ common-slim = enabled;
+ };
+
+ cli-apps = {
+ neovim = enabled;
+ tmux = enabled;
+ };
+ };
+ };
+}
diff --git a/modules/nixos/archetypes/workstation/default.nix b/modules/nixos/archetypes/workstation/default.nix
new file mode 100644
index 0000000..b487c41
--- /dev/null
+++ b/modules/nixos/archetypes/workstation/default.nix
@@ -0,0 +1,29 @@
+{ options, config, lib, pkgs, ... }:
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.archetypes.workstation;
+in
+{
+ options.plusultra.archetypes.workstation = with types; {
+ enable =
+ mkBoolOpt false "Whether or not to enable the workstation archetype.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra = {
+ suites = {
+ common = enabled;
+ desktop = enabled;
+ development = enabled;
+ art = enabled;
+ video = enabled;
+ social = enabled;
+ media = enabled;
+ };
+
+ tools = {
+ appimage-run = enabled;
+ };
+ };
+ };
+}
diff --git a/modules/nixos/cache/public/default.nix b/modules/nixos/cache/public/default.nix
new file mode 100644
index 0000000..f06e89b
--- /dev/null
+++ b/modules/nixos/cache/public/default.nix
@@ -0,0 +1,18 @@
+{ config, lib, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.cache.public;
+in
+{
+ options.plusultra.cache.public = {
+ enable = mkEnableOption "Plus Ultra public cache";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra.nix.extra-substituters = {
+ "https://attic.ruby.hamho.me/public".key = "public:QUkZTErD8fx9HQ64kuuEUZHO9tXNzws7chV8qy/KLUk=";
+ };
+ };
+}
diff --git a/modules/nixos/cli-apps/flake/default.nix b/modules/nixos/cli-apps/flake/default.nix
new file mode 100644
index 0000000..28559c6
--- /dev/null
+++ b/modules/nixos/cli-apps/flake/default.nix
@@ -0,0 +1,18 @@
+inputs@{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.cli-apps.flake;
+in
+{
+ options.plusultra.cli-apps.flake = with types; {
+ enable = mkBoolOpt false "Whether or not to enable flake.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [
+ snowfallorg.flake
+ ];
+ };
+}
diff --git a/modules/nixos/cli-apps/neovim/default.nix b/modules/nixos/cli-apps/neovim/default.nix
new file mode 100644
index 0000000..be46ce9
--- /dev/null
+++ b/modules/nixos/cli-apps/neovim/default.nix
@@ -0,0 +1,47 @@
+inputs @ { options
+, config
+, lib
+, pkgs
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.cli-apps.neovim;
+in
+{
+ options.plusultra.cli-apps.neovim = with types; {
+ enable = mkBoolOpt false "Whether or not to enable neovim.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [
+ # FIXME: As of today (2022-12-09), `page` no longer works with my Neovim
+ # configuration. Either something in my configuration is breaking it or `page` is busted.
+ # page
+ plusultra.neovim
+ ];
+
+ environment.variables = {
+ # PAGER = "page";
+ # MANPAGER =
+ # "page -C -e 'au User PageDisconnect sleep 100m|%y p|enew! |bd! #|pu p|set ft=man'";
+ PAGER = "less";
+ MANPAGER = "less";
+ NPM_CONFIG_PREFIX = "$HOME/.npm-global";
+ EDITOR = "nvim";
+ };
+
+ plusultra.home = {
+ configFile = {
+ "dashboard-nvim/.keep".text = "";
+ };
+
+ extraOptions = {
+ # Use Neovim for Git diffs.
+ programs.zsh.shellAliases.vimdiff = "nvim -d";
+ programs.bash.shellAliases.vimdiff = "nvim -d";
+ programs.fish.shellAliases.vimdiff = "nvim -d";
+ };
+ };
+ };
+}
diff --git a/modules/nixos/cli-apps/prisma/default.nix b/modules/nixos/cli-apps/prisma/default.nix
new file mode 100644
index 0000000..2b66371
--- /dev/null
+++ b/modules/nixos/cli-apps/prisma/default.nix
@@ -0,0 +1,31 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.cli-apps.prisma;
+in
+{
+ options.plusultra.cli-apps.prisma = with types; {
+ enable = mkBoolOpt false "Whether or not to install Prisma";
+ pkgs = {
+ npm = mkOpt package pkgs.nodePackages.prisma "The NPM package to install";
+ engines = mkOpt package pkgs.prisma-engines
+ "The package to get prisma engines from";
+ };
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = [ cfg.pkgs.npm ];
+
+ plusultra.home.extraOptions = {
+ programs.zsh.initExtra = ''
+ export PRISMA_MIGRATION_ENGINE_BINARY="${cfg.pkgs.engines}/bin/migration-engine"
+ export PRISMA_QUERY_ENGINE_BINARY="${cfg.pkgs.engines}/bin/query-engine"
+ export PRISMA_QUERY_ENGINE_LIBRARY="${cfg.pkgs.engines}/lib/libquery_engine.node"
+ export PRISMA_INTROSPECTION_ENGINE_BINARY="${cfg.pkgs.engines}/bin/introspection-engine"
+ export PRISMA_FMT_BINARY="${cfg.pkgs.engines}/bin/prisma-fmt"
+ '';
+ };
+ };
+}
diff --git a/modules/nixos/cli-apps/proton/default.nix b/modules/nixos/cli-apps/proton/default.nix
new file mode 100644
index 0000000..7e7c6ce
--- /dev/null
+++ b/modules/nixos/cli-apps/proton/default.nix
@@ -0,0 +1,16 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.cli-apps.proton;
+in
+{
+ options.plusultra.cli-apps.proton = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Proton.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [ proton-caller ];
+ };
+}
diff --git a/modules/nixos/cli-apps/thaw/default.nix b/modules/nixos/cli-apps/thaw/default.nix
new file mode 100644
index 0000000..2e0e1e9
--- /dev/null
+++ b/modules/nixos/cli-apps/thaw/default.nix
@@ -0,0 +1,20 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.cli-apps.thaw;
+in
+{
+ options.plusultra.cli-apps.thaw = with types; {
+ enable = mkBoolOpt false "Whether or not to enable thaw.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [
+ snowfallorg.thaw
+ ];
+ };
+}
diff --git a/modules/nixos/cli-apps/tmux/config/continuum.tmux b/modules/nixos/cli-apps/tmux/config/continuum.tmux
new file mode 100644
index 0000000..4b178e8
--- /dev/null
+++ b/modules/nixos/cli-apps/tmux/config/continuum.tmux
@@ -0,0 +1,5 @@
+# Enable saving sessions.
+set -g @continuum-restore 'on'
+
+# Save every 30 minutes.
+set -g @continuum-save-interval '30'
diff --git a/modules/nixos/cli-apps/tmux/config/extrakto.tmux b/modules/nixos/cli-apps/tmux/config/extrakto.tmux
new file mode 100644
index 0000000..e153d22
--- /dev/null
+++ b/modules/nixos/cli-apps/tmux/config/extrakto.tmux
@@ -0,0 +1,15 @@
+# Create a vertical split to show search & results to keep
+# the content visible.
+set -g @extrakto_split_direction "v"
+
+# Override the way that Extrakto copies text. By default
+# it was trying to use xclip and would not properly pick
+# up on $XDG_SESSION_TYPE being wayland. Instead, use
+# Tmux's built-in clipboard functionality.
+set -g @extrakto_clip_tool_run "tmux_osc52"
+
+# FIXME: The current version of Extrakto in
+# NixPkgs is out of date and does not support wayland.
+# This overrides the clipping tool to ensure that it works
+# under wayland.
+set -g @extrakto_clip_tool "wl-copy"
diff --git a/modules/nixos/cli-apps/tmux/config/fzf.tmux b/modules/nixos/cli-apps/tmux/config/fzf.tmux
new file mode 100644
index 0000000..4252bf6
--- /dev/null
+++ b/modules/nixos/cli-apps/tmux/config/fzf.tmux
@@ -0,0 +1,2 @@
+# Change default keybinding.
+TMUX_FZF_LAUNCH_KEY="C-f"
diff --git a/modules/nixos/cli-apps/tmux/config/general.tmux b/modules/nixos/cli-apps/tmux/config/general.tmux
new file mode 100644
index 0000000..2db919d
--- /dev/null
+++ b/modules/nixos/cli-apps/tmux/config/general.tmux
@@ -0,0 +1,38 @@
+# Lower delay waiting for chord after escape key press.
+set -g escape-time 0
+
+# Change the prefix from C-b to C-s to make it easier to type.
+set -g prefix C-s
+unbind C-b
+bind C-s send-prefix
+
+# Start window numbers at 1 rather than 0.
+set -g base-index 1
+
+# Use h, j, k, l for movement between panes.
+bind h select-pane -L
+bind j select-pane -D
+bind k select-pane -U
+bind l select-pane -R
+
+# Fix colors being wrong in programs like Neovim.
+set-option -ga terminal-overrides ",xterm-256color:Tc"
+
+# Expand the left status to accomodate longer session names.
+set-option -g status-left-length 20
+
+# One of the plugins binds C-l, make sure we have accces to it.
+unbind C-l
+bind -n C-l send-keys C-l
+
+# Don't require a prompt to detach from the current session.
+unbind -n M-E
+bind -n M-E detach-client
+
+# Reload tmux configuration from ~/.config/tmux/tmux.conf instead
+# of Tilish's default of ~/.tmux.conf.
+unbind -n M-C
+bind -n M-C source-file "~/.config/tmux/tmux.conf"
+
+# Use M-z to zoom and unzoom panes.
+bind -n M-z resize-pane -Z
diff --git a/modules/nixos/cli-apps/tmux/config/navigator.tmux b/modules/nixos/cli-apps/tmux/config/navigator.tmux
new file mode 100644
index 0000000..3358729
--- /dev/null
+++ b/modules/nixos/cli-apps/tmux/config/navigator.tmux
@@ -0,0 +1,21 @@
+# Smart pane switching with awareness of Vim splits.
+# See: https://github.com/christoomey/vim-tmux-navigator
+is_vim="ps -o state= -o comm= -t '#{pane_tty}' \
+ | grep -iqE '^[^TXZ ]+ +(\\S+\\/)?g?(view|n?vim?x?)(diff)?$'"
+
+bind-key -n 'M-h' if-shell "$is_vim" 'send-keys M-h' 'select-pane -L'
+bind-key -n 'M-j' if-shell "$is_vim" 'send-keys M-j' 'select-pane -D'
+bind-key -n 'M-k' if-shell "$is_vim" 'send-keys M-k' 'select-pane -U'
+bind-key -n 'M-l' if-shell "$is_vim" 'send-keys M-l' 'select-pane -R'
+tmux_version='$(tmux -V | sed -En "s/^tmux ([0-9]+(.[0-9]+)?).*/\1/p")'
+if-shell -b '[ "$(echo "$tmux_version < 3.0" | bc)" = 1 ]' \
+ "bind-key -n 'M-\\' if-shell \"$is_vim\" 'send-keys M-\\' 'select-pane -l'"
+if-shell -b '[ "$(echo "$tmux_version >= 3.0" | bc)" = 1 ]' \
+ "bind-key -n 'M-\\' if-shell \"$is_vim\" 'send-keys M-\\\\' 'select-pane -l'"
+
+bind-key -T copy-mode-vi 'M-h' select-pane -L
+bind-key -T copy-mode-vi 'M-j' select-pane -D
+bind-key -T copy-mode-vi 'M-k' select-pane -U
+bind-key -T copy-mode-vi 'M-l' select-pane -R
+bind-key -T copy-mode-vi 'M-p' select-pane -l
+
diff --git a/modules/nixos/cli-apps/tmux/config/nord.tmux b/modules/nixos/cli-apps/tmux/config/nord.tmux
new file mode 100644
index 0000000..0905c11
--- /dev/null
+++ b/modules/nixos/cli-apps/tmux/config/nord.tmux
@@ -0,0 +1,37 @@
+
+# Copyright (C) 2017-present Arctic Ice Studio
+# Copyright (C) 2017-present Sven Greb
+# Copyright (C) 2022-present Jake Hamilton
+
+# Project: Nord tmux
+# Repository: https://github.com/arcticicestudio/nord-tmux
+# License: MIT
+
+#+----------------+
+#+ Plugin Support +
+#+----------------+
+#+--- tmux-prefix-highlight ---+
+set -g @prefix_highlight_output_prefix "#[fg=brightcyan]#[bg=black]#[nobold]#[noitalics]#[nounderscore]#[bg=brightcyan]#[fg=black]"
+set -g @prefix_highlight_output_suffix ""
+set -g @prefix_highlight_copy_mode_attr "fg=brightcyan,bg=black,bold"
+
+#+--------+
+#+ Status +
+#+--------+
+#+--- Bars ---+
+set -g status-left "#[fg=brightblack,bg=black]#[fg=white,bg=brightblack,bold] #S #[fg=brightblack,bg=black,nobold,noitalics,nounderscore]"
+set -g status-right "#{prefix_highlight}#[fg=brightblack,bg=black,nobold,noitalics,nounderscore]#[fg=white,bg=brightblack,nobold] #H #[fg=brightblack,bg=black,nobold]"
+
+#+--- Windows ---+
+set -g window-status-format "#[fg=brightblack,bg=black,nobold,noitalics,nounderscore]#[fg=white,bg=brightblack] #I#[fg=white,bg=brightblack,nobold,noitalics,nounderscore]: #W #[fg=brightblack,bg=black,nobold,noitalics,nounderscore]"
+set -g window-status-current-format "#[fg=#5e81ac,bg=black]#[fg=white,bg=#5e81ac,bold,noitalics,nounderscore] #I#[fg=white,bg=#5e81ac,bold,noitalics,nounderscore]: #W #[fg=#5e81ac,bg=black,nobold,noitalics,nounderscore]"
+set -g window-status-separator " "
+
+# Center the window list (yes, this value has to be written as "centre").
+set -g status-justify centre
+
+#+----------------+
+#+ Windows +
+#+----------------+
+#+--- Bars ---+
+set -g pane-active-border-style "bg=default fg=blue"
diff --git a/modules/nixos/cli-apps/tmux/config/tilish.tmux b/modules/nixos/cli-apps/tmux/config/tilish.tmux
new file mode 100644
index 0000000..e676ac8
--- /dev/null
+++ b/modules/nixos/cli-apps/tmux/config/tilish.tmux
@@ -0,0 +1,12 @@
+# Integrate Tmux and Neovim movement.
+set -g @tilish-navigator "on"
+
+# Default to splitting once vertically and then splitting horizontally
+# after that.
+select-layout "main-vertical"
+select-layout -E
+set -g @tilish-default "main-vertical"
+
+
+bind-key -n "M-q" kill-pane
+
diff --git a/modules/nixos/cli-apps/tmux/default.nix b/modules/nixos/cli-apps/tmux/default.nix
new file mode 100644
index 0000000..6b4be15
--- /dev/null
+++ b/modules/nixos/cli-apps/tmux/default.nix
@@ -0,0 +1,20 @@
+{ lib
+, config
+, pkgs
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.cli-apps.tmux;
+in
+{
+ options.plusultra.cli-apps.tmux = {
+ enable = mkEnableOption "Tmux";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [
+ plusultra.tmux
+ ];
+ };
+}
diff --git a/modules/nixos/cli-apps/wine/default.nix b/modules/nixos/cli-apps/wine/default.nix
new file mode 100644
index 0000000..ec60403
--- /dev/null
+++ b/modules/nixos/cli-apps/wine/default.nix
@@ -0,0 +1,20 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.cli-apps.wine;
+in
+{
+ options.plusultra.cli-apps.wine = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Wine.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [
+ winePackages.unstable
+ winetricks
+ wine64Packages.unstable
+ ];
+ };
+}
diff --git a/modules/nixos/cli-apps/wshowkeys/default.nix b/modules/nixos/cli-apps/wshowkeys/default.nix
new file mode 100644
index 0000000..fea6e14
--- /dev/null
+++ b/modules/nixos/cli-apps/wshowkeys/default.nix
@@ -0,0 +1,17 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.cli-apps.wshowkeys;
+in
+{
+ options.plusultra.cli-apps.wshowkeys = with types; {
+ enable = mkBoolOpt false "Whether or not to enable wshowkeys.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra.user.extraGroups = [ "input" ];
+ environment.systemPackages = with pkgs; [ wshowkeys ];
+ };
+}
diff --git a/modules/nixos/cli-apps/yubikey/default.nix b/modules/nixos/cli-apps/yubikey/default.nix
new file mode 100644
index 0000000..0f549fa
--- /dev/null
+++ b/modules/nixos/cli-apps/yubikey/default.nix
@@ -0,0 +1,17 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.cli-apps.yubikey;
+in
+{
+ options.plusultra.cli-apps.yubikey = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Yubikey.";
+ };
+
+ config = mkIf cfg.enable {
+ services.yubikey-agent.enable = true;
+ environment.systemPackages = with pkgs; [ yubikey-manager ];
+ };
+}
diff --git a/modules/nixos/desktop/addons/electron-support/default.nix b/modules/nixos/desktop/addons/electron-support/default.nix
new file mode 100644
index 0000000..79410c7
--- /dev/null
+++ b/modules/nixos/desktop/addons/electron-support/default.nix
@@ -0,0 +1,19 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.desktop.addons.electron-support;
+in
+{
+ options.plusultra.desktop.addons.electron-support = with types; {
+ enable = mkBoolOpt false
+ "Whether to enable electron support in the desktop environment.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra.home.configFile."electron-flags.conf".source =
+ ./electron-flags.conf;
+
+ environment.sessionVariables = { NIXOS_OZONE_WL = "1"; };
+ };
+}
diff --git a/modules/nixos/desktop/addons/electron-support/electron-flags.conf b/modules/nixos/desktop/addons/electron-support/electron-flags.conf
new file mode 100644
index 0000000..fa9e252
--- /dev/null
+++ b/modules/nixos/desktop/addons/electron-support/electron-flags.conf
@@ -0,0 +1,2 @@
+--enable-features=UseOzonePlatform
+--ozone-platform=wayland
\ No newline at end of file
diff --git a/modules/nixos/desktop/addons/firefox-nordic-theme/default.nix b/modules/nixos/desktop/addons/firefox-nordic-theme/default.nix
new file mode 100644
index 0000000..86033fe
--- /dev/null
+++ b/modules/nixos/desktop/addons/firefox-nordic-theme/default.nix
@@ -0,0 +1,23 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.desktop.addons.firefox-nordic-theme;
+ profileDir = ".mozilla/firefox/${config.plusultra.user.name}";
+in
+{
+ options.plusultra.desktop.addons.firefox-nordic-theme = with types; {
+ enable = mkBoolOpt false "Whether to enable the Nordic theme for firefox.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra.apps.firefox = {
+ extraConfig = builtins.readFile
+ "${pkgs.plusultra.firefox-nordic-theme}/configuration/user.js";
+ userChrome = ''
+ @import "${pkgs.plusultra.firefox-nordic-theme}/userChrome.css";
+ '';
+ };
+ };
+}
diff --git a/modules/nixos/desktop/addons/foot/default.nix b/modules/nixos/desktop/addons/foot/default.nix
new file mode 100644
index 0000000..5336ad0
--- /dev/null
+++ b/modules/nixos/desktop/addons/foot/default.nix
@@ -0,0 +1,20 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.desktop.addons.foot;
+in
+{
+ options.plusultra.desktop.addons.foot = with types; {
+ enable = mkBoolOpt false "Whether to enable the gnome file manager.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra.desktop.addons.term = {
+ enable = true;
+ pkg = pkgs.foot;
+ };
+
+ plusultra.home.configFile."foot/foot.ini".source = ./foot.ini;
+ };
+}
diff --git a/modules/nixos/desktop/addons/foot/foot.ini b/modules/nixos/desktop/addons/foot/foot.ini
new file mode 100644
index 0000000..e12cc75
--- /dev/null
+++ b/modules/nixos/desktop/addons/foot/foot.ini
@@ -0,0 +1,50 @@
+###########################################
+#░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░#
+#░░█▀█░█░░░█░█░█▀▀░░░█░█░█░░░▀█▀░█▀▄░█▀█░░#
+#░░█▀▀░█░░░█░█░▀▀█░░░█░█░█░░░░█░░█▀▄░█▀█░░#
+#░░▀░░░▀▀▀░▀▀▀░▀▀▀░░░▀▀▀░▀▀▀░░▀░░▀░▀░▀░▀░░#
+#░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░#
+###########################################
+
+[main]
+font=Hack Nerd Font Mono:size=12
+#,Noto Color Emoji:size=12
+line-height=14
+underline-offset=2
+pad=20x4 center
+term=xterm-256color
+
+[scrollback]
+lines=2000
+
+[url]
+protocols=http,https,ftp,ftps,file,gemini,gopher,mailto
+
+[cursor]
+blink=yes
+
+[colors]
+# Nord
+foreground=D8DEE9
+background=2E3440
+
+regular0=2E3440
+regular1=BF616A
+regular2=A3BE8C
+regular3=EBCB8B
+regular4=81A1C1
+regular5=B48EAD
+regular6=88C0D0
+regular7=E5E9F0
+
+bright0=4C566A
+bright1=BF616A
+bright2=A3BE8C
+bright3=EBCB8B
+bright4=8FBCBB
+bright5=B48EAD
+bright6=8FBCBB
+bright7=ECEFF4
+
+[csd]
+size=0
diff --git a/modules/nixos/desktop/addons/gtk/default.nix b/modules/nixos/desktop/addons/gtk/default.nix
new file mode 100644
index 0000000..477b2e3
--- /dev/null
+++ b/modules/nixos/desktop/addons/gtk/default.nix
@@ -0,0 +1,118 @@
+{ options
+, config
+, lib
+, pkgs
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.desktop.addons.gtk;
+ gdmCfg = config.services.xserver.displayManager.gdm;
+in
+{
+ options.plusultra.desktop.addons.gtk = with types; {
+ enable = mkBoolOpt false "Whether to customize GTK and apply themes.";
+ theme = {
+ name =
+ mkOpt str "Nordic-darker"
+ "The name of the GTK theme to apply.";
+ pkg = mkOpt package pkgs.nordic "The package to use for the theme.";
+ };
+ cursor = {
+ name =
+ mkOpt str "Bibata-Modern-Ice"
+ "The name of the cursor theme to apply.";
+ pkg = mkOpt package pkgs.plusultra.bibata-cursors "The package to use for the cursor theme.";
+ };
+ icon = {
+ name =
+ mkOpt str "Papirus"
+ "The name of the icon theme to apply.";
+ pkg = mkOpt package pkgs.papirus-icon-theme "The package to use for the icon theme.";
+ };
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = [
+ cfg.icon.pkg
+ cfg.cursor.pkg
+ ];
+
+ environment.sessionVariables = {
+ XCURSOR_THEME = cfg.cursor.name;
+ };
+
+ plusultra.home.extraOptions = {
+ gtk = {
+ enable = true;
+
+ theme = {
+ name = cfg.theme.name;
+ package = cfg.theme.pkg;
+ };
+
+ cursorTheme = {
+ name = cfg.cursor.name;
+ package = cfg.cursor.pkg;
+ };
+
+ iconTheme = {
+ name = cfg.icon.name;
+ package = cfg.icon.pkg;
+ };
+ };
+ };
+
+ # NOTE: In order to set the cursor theme in GDM we have to specify it in the
+ # dconf profile. However, the NixOS module doesn't provide an easy way to do this so the relevant
+ # parts have been extracted from:
+ # https://github.com/NixOS/nixpkgs/blob/96e18717904dfedcd884541e5a92bf9ff632cf39/nixos/modules/services/x11/display-managers/gdm.nix
+ #
+ # NOTE: The GTK and icon themes don't seem to affect recent GDM versions. I've
+ # left them here as reference for the future.
+ programs.dconf.profiles = mkIf gdmCfg.enable {
+ gdm =
+ let
+ customDconf = pkgs.writeTextFile {
+ name = "gdm-dconf";
+ destination = "/dconf/gdm-custom";
+ text = ''
+ ${optionalString (!gdmCfg.autoSuspend) ''
+ [org/gnome/settings-daemon/plugins/power]
+ sleep-inactive-ac-type='nothing'
+ sleep-inactive-battery-type='nothing'
+ sleep-inactive-ac-timeout=0
+ sleep-inactive-battery-timeout=0
+ ''}
+
+ [org/gnome/desktop/interface]
+ gtk-theme='${cfg.theme.name}'
+ cursor-theme='${cfg.cursor.name}'
+ icon-theme='${cfg.icon.name}'
+ '';
+ };
+
+ customDconfDb = pkgs.stdenv.mkDerivation {
+ name = "gdm-dconf-db";
+ buildCommand = ''
+ ${pkgs.dconf}/bin/dconf compile $out ${customDconf}/dconf
+ '';
+ };
+ in
+ mkForce (
+ pkgs.stdenv.mkDerivation {
+ name = "dconf-gdm-profile";
+ buildCommand = ''
+ # Check that the GDM profile starts with what we expect.
+ if [ $(head -n 1 ${pkgs.gnome.gdm}/share/dconf/profile/gdm) != "user-db:user" ]; then
+ echo "GDM dconf profile changed, please update gtk/default.nix"
+ exit 1
+ fi
+ # Insert our custom DB behind it.
+ sed '2ifile-db:${customDconfDb}' ${pkgs.gnome.gdm}/share/dconf/profile/gdm > $out
+ '';
+ }
+ );
+ };
+ };
+}
diff --git a/modules/nixos/desktop/addons/kanshi/config b/modules/nixos/desktop/addons/kanshi/config
new file mode 100644
index 0000000..ea1aab6
--- /dev/null
+++ b/modules/nixos/desktop/addons/kanshi/config
@@ -0,0 +1,3 @@
+profile {
+ output eDP-1 enable scale 2
+}
diff --git a/modules/nixos/desktop/addons/kanshi/default.nix b/modules/nixos/desktop/addons/kanshi/default.nix
new file mode 100644
index 0000000..9c1e6a0
--- /dev/null
+++ b/modules/nixos/desktop/addons/kanshi/default.nix
@@ -0,0 +1,41 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.desktop.addons.kanshi;
+ user = config.plusultra.user;
+ home = config.users.users.${user.name}.home;
+in
+{
+ options.plusultra.desktop.addons.kanshi = with types; {
+ enable =
+ mkBoolOpt false "Whether to enable Kanshi in the desktop environment.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra.home.configFile."kanshi/config".source = ./config;
+
+ environment.systemPackages = with pkgs; [ kanshi ];
+
+ # configuring kanshi
+ systemd.user.services.kanshi = {
+ description = "Kanshi output autoconfig ";
+ wantedBy = [ "graphical-session.target" ];
+ partOf = [ "graphical-session.target" ];
+ environment = { XDG_CONFIG_HOME = "${home}/.config"; };
+ serviceConfig = {
+ ExecCondition = ''
+ ${pkgs.bash}/bin/bash -c '[ -n "$WAYLAND_DISPLAY" ]'
+ '';
+
+ ExecStart = ''
+ ${pkgs.kanshi}/bin/kanshi
+ '';
+
+ RestartSec = 5;
+ Restart = "always";
+ };
+ };
+ };
+}
diff --git a/modules/nixos/desktop/addons/keyring/default.nix b/modules/nixos/desktop/addons/keyring/default.nix
new file mode 100644
index 0000000..4629edd
--- /dev/null
+++ b/modules/nixos/desktop/addons/keyring/default.nix
@@ -0,0 +1,17 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.desktop.addons.keyring;
+in
+{
+ options.plusultra.desktop.addons.keyring = with types; {
+ enable = mkBoolOpt false "Whether to enable the gnome keyring.";
+ };
+
+ config = mkIf cfg.enable {
+ services.gnome.gnome-keyring.enable = true;
+
+ environment.systemPackages = with pkgs; [ gnome.seahorse ];
+ };
+}
diff --git a/modules/nixos/desktop/addons/mako/config b/modules/nixos/desktop/addons/mako/config
new file mode 100644
index 0000000..84b6aa0
--- /dev/null
+++ b/modules/nixos/desktop/addons/mako/config
@@ -0,0 +1,18 @@
+###########################################
+#░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░#
+#░░█▀█░█░░░█░█░█▀▀░░░█░█░█░░░▀█▀░█▀▄░█▀█░░#
+#░░█▀▀░█░░░█░█░▀▀█░░░█░█░█░░░░█░░█▀▄░█▀█░░#
+#░░▀░░░▀▀▀░▀▀▀░▀▀▀░░░▀▀▀░▀▀▀░░▀░░▀░▀░▀░▀░░#
+#░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░#
+###########################################
+
+font=Hack Nerd Font Mono 10
+border-radius=8
+text-color=#2e3440ff
+background-color=#eceff4f4
+border-color=#d8dee9ff
+border-size=0
+margin=12,12,6
+padding=12,12,12,12
+default-timeout=5000
+max-visible=3
diff --git a/modules/nixos/desktop/addons/mako/default.nix b/modules/nixos/desktop/addons/mako/default.nix
new file mode 100644
index 0000000..12e1f15
--- /dev/null
+++ b/modules/nixos/desktop/addons/mako/default.nix
@@ -0,0 +1,44 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.desktop.addons.mako;
+in
+{
+ options.plusultra.desktop.addons.mako = with types; {
+ enable = mkBoolOpt false "Whether to enable Mako in Sway.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [ mako libnotify ];
+
+ systemd.user.services.mako = {
+ description = "Mako notification daemon";
+ wantedBy = [ "graphical-session.target" ];
+ partOf = [ "graphical-session.target" ];
+ after = [ "graphical-session.target" ];
+ serviceConfig = {
+ Type = "dbus";
+ BusName = "org.freedesktop.Notifications";
+
+ ExecCondition = ''
+ ${pkgs.bash}/bin/bash -c '[ -n "$WAYLAND_DISPLAY" ]'
+ '';
+
+ ExecStart = ''
+ ${pkgs.mako}/bin/mako
+ '';
+
+ ExecReload = ''
+ ${pkgs.mako}/bin/makoctl reload
+ '';
+
+ Restart = "on-failure";
+ RestartSec = 1;
+ TimeoutStopSec = 10;
+ };
+ };
+
+ plusultra.home.configFile."mako/config".source = ./config;
+ };
+}
diff --git a/modules/nixos/desktop/addons/nautilus/default.nix b/modules/nixos/desktop/addons/nautilus/default.nix
new file mode 100644
index 0000000..df48a7d
--- /dev/null
+++ b/modules/nixos/desktop/addons/nautilus/default.nix
@@ -0,0 +1,20 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.desktop.addons.nautilus;
+in
+{
+ options.plusultra.desktop.addons.nautilus = with types; {
+ enable = mkBoolOpt false "Whether to enable the gnome file manager.";
+ };
+
+ config = mkIf cfg.enable {
+ # Enable support for browsing samba shares.
+ services.gvfs.enable = true;
+ networking.firewall.extraCommands =
+ "iptables -t raw -A OUTPUT -p udp -m udp --dport 137 -j CT --helper netbios-ns";
+
+ environment.systemPackages = with pkgs; [ gnome.nautilus ];
+ };
+}
diff --git a/modules/nixos/desktop/addons/rofi/config.rasi b/modules/nixos/desktop/addons/rofi/config.rasi
new file mode 100644
index 0000000..25fe407
--- /dev/null
+++ b/modules/nixos/desktop/addons/rofi/config.rasi
@@ -0,0 +1,42 @@
+/*
+vim: filetype=css
+ */
+
+configuration {
+ fullscreen: false;
+ show-icons: false;
+ sidebar-mode: false;
+}
+
+* {
+ // Default bg is transparent.
+ background-color: transparent;
+ // Default text is white
+ text-color: white;
+ spacing: 30;
+}
+
+#window {
+ // Default font
+ font: "Nerd Font Hack 18";
+ fullscreen: true;
+ transparency: "background";
+
+ background-color: #282a36BA;
+
+ // Add dummy widgets on top and bottom so the sizing
+ // nicely centers hdum, independent of resolution.
+ children: [ dummy1, hdum, dummy2 ];
+}
+
+#hdum {
+ orientation: horizontal;
+ // Add dummy widgets on left and right so the sizing
+ // nicely centers mainbox, independent of resolution.
+ children: [ dummy3, mainbox, dummy4 ];
+}
+
+#element selected {
+ text-color: #caa9fa;
+}
+
diff --git a/modules/nixos/desktop/addons/rofi/default.nix b/modules/nixos/desktop/addons/rofi/default.nix
new file mode 100644
index 0000000..f6c65f1
--- /dev/null
+++ b/modules/nixos/desktop/addons/rofi/default.nix
@@ -0,0 +1,18 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.desktop.addons.rofi;
+in
+{
+ options.plusultra.desktop.addons.rofi = with types; {
+ enable =
+ mkBoolOpt false "Whether to enable Rofi in the desktop environment.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [ rofi ];
+
+ plusultra.home.configFile."rofi/config.rasi".source = ./config.rasi;
+ };
+}
diff --git a/modules/nixos/desktop/addons/swappy/config b/modules/nixos/desktop/addons/swappy/config
new file mode 100644
index 0000000..632dc00
--- /dev/null
+++ b/modules/nixos/desktop/addons/swappy/config
@@ -0,0 +1,3 @@
+[Default]
+save_dir=$HOME/Pictures/screenshots
+save_filename_format=%Y%m%d-%H%M%S.png
diff --git a/modules/nixos/desktop/addons/swappy/default.nix b/modules/nixos/desktop/addons/swappy/default.nix
new file mode 100644
index 0000000..3bce885
--- /dev/null
+++ b/modules/nixos/desktop/addons/swappy/default.nix
@@ -0,0 +1,19 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.desktop.addons.swappy;
+in
+{
+ options.plusultra.desktop.addons.swappy = with types; {
+ enable =
+ mkBoolOpt false "Whether to enable Swappy in the desktop environment.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [ swappy ];
+
+ plusultra.home.configFile."swappy/config".source = ./config;
+ plusultra.home.file."Pictures/screenshots/.keep".text = "";
+ };
+}
diff --git a/modules/nixos/desktop/addons/term/default.nix b/modules/nixos/desktop/addons/term/default.nix
new file mode 100644
index 0000000..862a637
--- /dev/null
+++ b/modules/nixos/desktop/addons/term/default.nix
@@ -0,0 +1,14 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.desktop.addons.term;
+in
+{
+ options.plusultra.desktop.addons.term = with types; {
+ enable = mkBoolOpt false "Whether to enable the gnome file manager.";
+ pkg = mkOpt package pkgs.foot "The terminal to install.";
+ };
+
+ config = mkIf cfg.enable { environment.systemPackages = [ cfg.pkg ]; };
+}
diff --git a/modules/nixos/desktop/addons/wallpapers/default.nix b/modules/nixos/desktop/addons/wallpapers/default.nix
new file mode 100644
index 0000000..4fc9d0d
--- /dev/null
+++ b/modules/nixos/desktop/addons/wallpapers/default.nix
@@ -0,0 +1,26 @@
+{ options, config, pkgs, lib, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.desktop.addons.wallpapers;
+ inherit (pkgs.plusultra) wallpapers;
+in
+{
+ options.plusultra.desktop.addons.wallpapers = with types; {
+ enable = mkBoolOpt false
+ "Whether or not to add wallpapers to ~/Pictures/wallpapers.";
+ };
+
+ config = {
+ plusultra.home.file = lib.foldl
+ (acc: name:
+ let wallpaper = wallpapers.${name};
+ in
+ acc // {
+ "Pictures/wallpapers/${wallpaper.fileName}".source = wallpaper;
+ })
+ { }
+ (wallpapers.names);
+ };
+}
diff --git a/modules/nixos/desktop/addons/waybar/config b/modules/nixos/desktop/addons/waybar/config
new file mode 100644
index 0000000..88e2df4
--- /dev/null
+++ b/modules/nixos/desktop/addons/waybar/config
@@ -0,0 +1,119 @@
+{
+ "layer": "top",
+ "modules-left": [
+ "sway/workspaces",
+ "custom/media",
+ "sway/mode"
+ ],
+ "modules-center": [],
+ "modules-right": [
+ "network",
+ "pulseaudio",
+ "cpu",
+ "battery",
+ "tray",
+ "clock#date",
+ "clock#time"
+ ],
+ "sway/mode": {
+ "format": "{}"
+ },
+ "custom/media": {
+ "format": "{icon}",
+ "return-type": "json",
+ "format-icons": {
+ "Playing": "",
+ "Paused": "ﳌ"
+ },
+ "max-length": 70,
+ "exec": "playerctl -a metadata --format '{\"text\": \"{{playerName}}\", \"tooltip\": \"{{playerName}}: {{markup_escape(title)}}\", \"alt\": \"{{status}}\", \"class\": \"{{status}}\"}' -F",
+ "on-click": "playerctl play-pause"
+ },
+ "mpd": {
+ "tooltip": false,
+ "format": "{stateIcon} {artist} - {album} - {title} ({elapsedTime:%M:%S}/{totalTime:%M:%S})",
+ "format-disconnected": "ﳌ",
+ "format-stopped": "",
+ "state-icons": {
+ "playing": "",
+ "paused": ""
+ }
+ },
+ "pulseaudio": {
+ "tooltip": true,
+ "tooltip-format": "{volume}%",
+ "scroll-step": 5,
+ "format": "{icon}",
+ "format-bluetooth": "",
+ "format-muted": "婢",
+ "format-icons": {
+ "default": [
+ "奄",
+ "奔",
+ "墳"
+ ]
+ },
+ "on-click": "gnome-control-center sound"
+ },
+ "network": {
+ "tooltip": true,
+ "format-wifi": " ",
+ "tooltip-format-wifi": "{essid} @ {signalStrength}%",
+ "format-ethernet": "",
+ "on-click": "gnome-control-center wifi"
+ },
+ "temperature": {
+ "tooltip": true,
+ "tooltip-format": "{temperatureC} 糖",
+ "critical-threshold": 70,
+ "format-icons": [
+ "",
+ "",
+ ""
+ ],
+ "format": "{icon}"
+ },
+ "cpu": {
+ "tooltip": true,
+ "format": "",
+ "states": {
+ "heavy": 70,
+ "full": 90
+ }
+ },
+ "memory": {
+ "tooltip": true,
+ "tooltip-format": "{used:0.1f}G/{total:0.1f}G",
+ "format": "",
+ "states": {
+ "heavy": 70,
+ "full": 90
+ }
+ },
+ "battery": {
+ "bat": "BAT1",
+ "interval": 60,
+ "states": {
+ "warning": 30,
+ "critical": 15
+ },
+ "format": "{icon}",
+ "format-critical": "",
+ "format-icons": {
+ "default": ["", "", "", "", "", "", "", "", "", ""],
+ "charging": ["", "", "", "", "", "", ""],
+ "not": "",
+ "plugged": ""
+ }
+ },
+ "tray": {
+ "icon-size": 16,
+ "spacing": 10
+ },
+ "clock#date": {
+ "format": "{:%a, %b %d}"
+ },
+ "clock#time": {
+ "format": "{:%I:%M %p}"
+ }
+}
diff --git a/modules/nixos/desktop/addons/waybar/default.nix b/modules/nixos/desktop/addons/waybar/default.nix
new file mode 100644
index 0000000..e04a6b8
--- /dev/null
+++ b/modules/nixos/desktop/addons/waybar/default.nix
@@ -0,0 +1,19 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.desktop.addons.waybar;
+in
+{
+ options.plusultra.desktop.addons.waybar = with types; {
+ enable =
+ mkBoolOpt false "Whether to enable Waybar in the desktop environment.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [ waybar ];
+
+ plusultra.home.configFile."waybar/config".source = ./config;
+ plusultra.home.configFile."waybar/style.css".source = ./style.css;
+ };
+}
diff --git a/modules/nixos/desktop/addons/waybar/style.css b/modules/nixos/desktop/addons/waybar/style.css
new file mode 100644
index 0000000..61170cc
--- /dev/null
+++ b/modules/nixos/desktop/addons/waybar/style.css
@@ -0,0 +1,314 @@
+/*
+ ********************************************
+ *░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░*
+ *░░█▀█░█░░░█░█░█▀▀░░░█░█░█░░░▀█▀░█▀▄░█▀█░░*
+ *░░█▀▀░█░░░█░█░▀▀█░░░█░█░█░░░░█░░█▀▄░█▀█░░*
+ *░░▀░░░▀▀▀░▀▀▀░▀▀▀░░░▀▀▀░▀▀▀░░▀░░▀░▀░▀░▀░░*
+ *░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░*
+ ********************************************
+ */
+
+* {
+ border: none;
+ border-radius: 0;
+ font-family: Nerd Font Hack;
+ font-size: 14px;
+}
+
+window#waybar {
+ background: transparent;
+}
+
+window#waybar.hidden {
+ opacity: 0.2;
+}
+
+#window {
+ margin-top: 8px;
+ padding-left: 20px;
+ padding-right: 20px;
+ border-radius: 8px;
+ transition: none;
+ color: transparent;
+ background: transparent;
+}
+
+window#waybar.termite #window,
+window#waybar.Firefox #window,
+window#waybar.Navigator #window,
+window#waybar.PCSX2 #window {
+ color: #4d4d4d;
+ background: #e6e6e6;
+}
+
+tooltip {
+ border-radius: 12px;
+ background: #eceff4;
+}
+
+tooltip label {
+ color: #2e3440;
+ text-shadow: none;
+ padding: 0 4px;
+}
+
+#workspaces button,
+#mode,
+#mpd,
+#custom-media,
+#network,
+#pulseaudio,
+#temperature,
+#cpu,
+#memory,
+#tray,
+#clock,
+#battery {
+ color: #2e3440;
+ background: #eceff4;
+}
+
+#workspaces {
+ margin-top: 8px;
+ margin-left: 12px;
+ margin-bottom: 0;
+ border-radius: 8px;
+ transition: none;
+ background: #d8dee9;
+}
+
+#workspaces button {
+ transition: none;
+ font-size: 16px;
+ border-radius: 8px;
+ padding: 4px 8px;
+ font-weight: bold;
+ background: #d8dee9;
+}
+
+#workspaces button.visible {
+ color: #88c0d0;
+ background: #eceff4;
+}
+
+#workspaces button.visible:hover {
+ color: #88c0d0;
+ background: #eceff4;
+}
+
+#workspaces button:hover {
+ transition: none;
+ box-shadow: inherit;
+ text-shadow: inherit;
+ color: #81a1c1;
+}
+
+#custom-media {
+ margin-top: 8px;
+ margin-left: 8px;
+ padding-left: 16px;
+ padding-right: 16px;
+ margin-bottom: 0;
+ border-radius: 8px;
+ transition: none;
+ font-weight: bold;
+}
+
+#custom-media.Paused {
+ color: #4c566a;
+ background: #d8dee9;
+}
+
+#custom-media.Playing {
+ color: #eceff4;
+ background: #8fbcbb;
+}
+
+#network {
+ margin-top: 8px;
+ margin-left: 8px;
+ padding-left: 16px;
+ padding-right: 10px;
+ margin-bottom: 0;
+ border-radius: 8px;
+ transition: none;
+ font-size: 24px;
+}
+
+#network.disconnected,
+#network.disabled {
+ color: #2e3440;
+ background: #eceff4;
+}
+
+#network.wifi {
+ color: #d8dee9;
+ background: #8fbcbb;
+}
+
+#pulseaudio {
+ margin-top: 8px;
+ margin-left: 8px;
+ padding-left: 16px;
+ padding-right: 16px;
+ margin-bottom: 0;
+ border-radius: 8px;
+ transition: none;
+ font-size: 18px;
+}
+
+#pulseaudio.bluetooth {
+ color: #d8dee9;
+ background: #81a1c1;
+}
+
+#pulseaudio.muted {
+ color: #d8dee9;
+ background: #bf616a;
+}
+
+#battery {
+ margin-top: 8px;
+ margin-left: 8px;
+ padding-left: 16px;
+ padding-right: 16px;
+ margin-bottom: 0;
+ border-radius: 8px;
+ transition: none;
+ font-size: 18px;
+}
+
+#battery.warning {
+ color: #eceff4;
+ background: #d08770;
+}
+
+#battery.critical {
+ color: #eceff4;
+ background: #bf616a;
+}
+
+#temperature {
+ margin-top: 8px;
+ margin-left: 8px;
+ padding-left: 16px;
+ padding-right: 16px;
+ margin-bottom: 0;
+ border-radius: 8px;
+ transition: none;
+ font-size: 20px;
+}
+
+#temperature.critical {
+ color: #d8dee9;
+ background: #bf616a;
+ animation: red-flash 1s linear infinite;
+}
+
+@keyframes red-flash {
+ 0% {
+ background: #bf616a;
+ }
+
+ 50% {
+ background: #d37f87;
+ }
+
+ 100% {
+ background: #bf616a;
+ }
+}
+
+#cpu {
+ margin-top: 8px;
+ margin-left: 8px;
+ padding-left: 16px;
+ padding-right: 16px;
+ margin-bottom: 0;
+ border-radius: 8px;
+ transition: none;
+ font-size: 20px;
+}
+
+#cpu.heavy {
+ color: #d8dee9;
+ background: #d08770;
+}
+
+#cpu.full {
+ color: #d8dee9;
+ background: #bf616a;
+}
+
+#memory {
+ margin-top: 8px;
+ margin-left: 8px;
+ padding-left: 16px;
+ padding-right: 16px;
+ margin-bottom: 0;
+ border-radius: 8px;
+ transition: none;
+ font-size: 20px;
+}
+
+#memory.heavy {
+ color: #d8dee9;
+ background: #d08770;
+}
+
+#memory.full {
+ color: #d8dee9;
+ background: #bf616a;
+}
+
+#tray {
+ margin-top: 8px;
+ margin-left: 8px;
+ padding-left: 16px;
+ padding-right: 16px;
+ margin-bottom: 0;
+ border-radius: 8px;
+ transition: none;
+ font-size: 20px;
+}
+
+#mode {
+ margin-top: 8px;
+ margin-left: 8px;
+ padding-left: 16px;
+ padding-right: 16px;
+ margin-bottom: 0;
+ border-radius: 8px;
+ transition: none;
+ /* font-size: 20px; */
+ color: #eceff4;
+ background: #d08770;
+ font-weight: bold;
+}
+
+#clock {
+ margin-top: 8px;
+ margin-bottom: 0;
+ transition: none;
+ font-weight: bold;
+}
+
+#clock.date {
+ margin-left: 8px;
+ border-radius: 8px;
+ padding-left: 16px;
+ padding-right: 10px;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ background: #e5e9f0;
+}
+
+#clock.time {
+ padding-left: 10px;
+ padding-right: 16px;
+ border-radius: 8px;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ margin-right: 12px;
+ background: #d8dee9;
+}
diff --git a/modules/nixos/desktop/addons/wofi/config b/modules/nixos/desktop/addons/wofi/config
new file mode 100644
index 0000000..fe03dbe
--- /dev/null
+++ b/modules/nixos/desktop/addons/wofi/config
@@ -0,0 +1,3 @@
+stylesheet=./style.css
+term=foot
+insensitive=true
diff --git a/modules/nixos/desktop/addons/wofi/default.nix b/modules/nixos/desktop/addons/wofi/default.nix
new file mode 100644
index 0000000..2b06577
--- /dev/null
+++ b/modules/nixos/desktop/addons/wofi/default.nix
@@ -0,0 +1,23 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.desktop.addons.wofi;
+in
+{
+ options.plusultra.desktop.addons.wofi = with types; {
+ enable =
+ mkBoolOpt false "Whether to enable the Wofi in the desktop environment.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [ wofi wofi-emoji ];
+
+ # config -> .config/wofi/config
+ # css -> .config/wofi/style.css
+ # colors -> $XDG_CACHE_HOME/wal/colors
+ # plusultra.home.configFile."foot/foot.ini".source = ./foot.ini;
+ plusultra.home.configFile."wofi/config".source = ./config;
+ plusultra.home.configFile."wofi/style.css".source = ./style.css;
+ };
+}
diff --git a/modules/nixos/desktop/addons/wofi/style.css b/modules/nixos/desktop/addons/wofi/style.css
new file mode 100644
index 0000000..d844501
--- /dev/null
+++ b/modules/nixos/desktop/addons/wofi/style.css
@@ -0,0 +1,127 @@
+window {
+ font-family: "Hack Nerd Font";
+ background: transparent;
+}
+
+#outer-box {
+ padding: 10px;
+ border-radius: 8px;
+ background: #2e3440;
+}
+
+#scroll {
+ /* The Nordic gtk theme adds an outline to show scroll areas... */
+ outline-color: transparent;
+}
+
+#input {
+ color: #e5e9f0;
+ caret-color: #e5e9f0;
+ background: #3b4252;
+ border-top-color: #3b4252;
+ border-left-color: #3b4252;
+ border-right-color: #3b4252;
+ border-bottom-color: #3b4252;
+ box-shadow: 0 0 0 1px transparent inset;
+ outline-color: transparent !important;
+}
+
+#input:focus {
+ background: #3b4252;
+ border-color: #3b4252 !important;
+ box-shadow: 0 0 0 1px transparent inset;
+ border-top-color: #3b4252 !important;
+ border-left-color: #3b4252 !important;
+ border-right-color: #3b4252 !important;
+ border-bottom-color: #3b4252 !important;
+ box-shadow: none !important;
+ outline-color: transparent !important;
+}
+
+#input image.left {
+ color: #d8dee9;
+}
+
+#input:focus image.left {
+ color: #e5e9f0;
+}
+
+#input image.right {
+ color: #d8dee9;
+}
+
+#input:focus image.right {
+ color: #e5e9f0;
+}
+
+label {
+ /* We set backgrounds on the block level. */
+ background: transparent;
+}
+
+#scroll {
+ padding-top: 6px;
+}
+
+#entry {
+ color: #4c566a;
+ padding: 8px 8px;
+ border-radius: 4px;
+ background: transparent;
+}
+
+#entry:selected {
+ color: #eceff4;
+ background: #8fbcbb;
+ font-weight: bold;
+}
+
+expander arrow {
+ margin-right: 8px;
+}
+
+#entry #selected #text {
+ color: #eceff4;
+}
+
+expander list {
+ margin-top: 8px;
+ /* background: #8fbcbb; */
+ background: transparent;
+ padding-left: 16px;
+}
+
+expander list #entry {
+ transition: none;
+ background: transparent;
+}
+
+expander list #entry:hover,
+expander list #entry:active {
+ /* color: #8fbcbb;
+ background: #e5e9f0; */
+}
+
+expander list #entry #selected {
+ background: #8fbcbb;
+}
+
+expander list #entry #selected label {
+ color: #eceff4;
+ font-weight: bold;
+}
+
+expander list #entry:hover,
+expander list #entry:active {
+ background: #8fbcbb;
+}
+
+expander list #entry:hover label,
+expander list #entry:active label {
+ color: #eceff4;
+ font-weight: bold;
+}
+
+expander list label {
+ color: #d8dee9;
+}
diff --git a/modules/nixos/desktop/addons/xdg-portal/default.nix b/modules/nixos/desktop/addons/xdg-portal/default.nix
new file mode 100644
index 0000000..dc09173
--- /dev/null
+++ b/modules/nixos/desktop/addons/xdg-portal/default.nix
@@ -0,0 +1,24 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.desktop.addons.xdg-portal;
+in
+{
+ options.plusultra.desktop.addons.xdg-portal = with types; {
+ enable = mkBoolOpt false "Whether or not to add support for xdg portal.";
+ };
+
+ config = mkIf cfg.enable {
+ xdg = {
+ portal = {
+ enable = true;
+ extraPortals = with pkgs; [
+ xdg-desktop-portal-wlr
+ xdg-desktop-portal-gtk
+ ];
+ gtkUsePortal = true;
+ };
+ };
+ };
+}
diff --git a/modules/nixos/desktop/gnome/default.nix b/modules/nixos/desktop/gnome/default.nix
new file mode 100644
index 0000000..51cd010
--- /dev/null
+++ b/modules/nixos/desktop/gnome/default.nix
@@ -0,0 +1,333 @@
+{ options
+, config
+, lib
+, pkgs
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.desktop.gnome;
+ gdmHome = config.users.users.gdm.home;
+
+ defaultExtensions = with pkgs.gnomeExtensions; [
+ appindicator
+ aylurs-widgets
+ dash-to-dock
+ emoji-selector
+ gsconnect
+ gtile
+ just-perfection
+ logo-menu
+ no-overview
+ remove-app-menu
+ space-bar
+ top-bar-organizer
+ wireless-hid
+
+ # NOTE: These extensions are currently unsupported. They may also
+ # no longer be required.
+
+ # audio-output-switcher
+ # big-avatar
+ # clear-top-bar
+ ];
+
+ default-attrs = mapAttrs (key: mkDefault);
+ nested-default-attrs = mapAttrs (key: default-attrs);
+in
+{
+ options.plusultra.desktop.gnome = with types; {
+ enable =
+ mkBoolOpt false "Whether or not to use Gnome as the desktop environment.";
+ wallpaper = {
+ light = mkOpt (oneOf [ str package ]) pkgs.plusultra.wallpapers.nord-rainbow-light-nix "The light wallpaper to use.";
+ dark = mkOpt (oneOf [ str package ]) pkgs.plusultra.wallpapers.nord-rainbow-dark-nix "The dark wallpaper to use.";
+ };
+ color-scheme = mkOpt (enum [ "light" "dark" ]) "dark" "The color scheme to use.";
+ wayland = mkBoolOpt true "Whether or not to use Wayland.";
+ suspend =
+ mkBoolOpt true "Whether or not to suspend the machine after inactivity.";
+ monitors = mkOpt (nullOr path) null "The monitors.xml file to create.";
+ extensions = mkOpt (listOf package) [ ] "Extra Gnome extensions to install.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra.system.xkb.enable = true;
+ plusultra.desktop.addons = {
+ gtk = enabled;
+ wallpapers = enabled;
+ electron-support = enabled;
+ foot = enabled;
+ };
+
+ environment.systemPackages = with pkgs;
+ [
+ (hiPrio plusultra.xdg-open-with-portal)
+ wl-clipboard
+ gnome.gnome-tweaks
+ gnome.nautilus-python
+ ]
+ ++ defaultExtensions
+ ++ cfg.extensions;
+
+ environment.gnome.excludePackages = with pkgs.gnome; [
+ pkgs.gnome-tour
+ epiphany
+ geary
+ gnome-font-viewer
+ gnome-system-monitor
+ gnome-maps
+ ];
+
+ systemd.tmpfiles.rules =
+ [
+ "d ${gdmHome}/.config 0711 gdm gdm"
+ ]
+ ++ (
+ # "./monitors.xml" comes from ~/.config/monitors.xml when GNOME
+ # display information is updated.
+ lib.optional (cfg.monitors != null) "L+ ${gdmHome}/.config/monitors.xml - - - - ${cfg.monitors}"
+ );
+
+ systemd.services.plusultra-user-icon = {
+ before = [ "display-manager.service" ];
+ wantedBy = [ "display-manager.service" ];
+
+ serviceConfig = {
+ Type = "simple";
+ User = "root";
+ Group = "root";
+ };
+
+ script = ''
+ config_file=/var/lib/AccountsService/users/${config.plusultra.user.name}
+ icon_file=/run/current-system/sw/share/plusultra-icons/user/${config.plusultra.user.name}/${config.plusultra.user.icon.fileName}
+
+ if ! [ -d "$(dirname "$config_file")"]; then
+ mkdir -p "$(dirname "$config_file")"
+ fi
+
+ if ! [ -f "$config_file" ]; then
+ echo "[User]
+ Session=gnome
+ SystemAccount=false
+ Icon=$icon_file" > "$config_file"
+ else
+ icon_config=$(sed -E -n -e "/Icon=.*/p" $config_file)
+
+ if [[ "$icon_config" == "" ]]; then
+ echo "Icon=$icon_file" >> $config_file
+ else
+ sed -E -i -e "s#^Icon=.*$#Icon=$icon_file#" $config_file
+ fi
+ fi
+ '';
+ };
+
+ # Required for app indicators
+ services.udev.packages = with pkgs; [ gnome3.gnome-settings-daemon ];
+
+ services.xserver = {
+ enable = true;
+
+ libinput.enable = true;
+ displayManager.gdm = {
+ enable = true;
+ wayland = cfg.wayland;
+ autoSuspend = cfg.suspend;
+ };
+ desktopManager.gnome.enable = true;
+ };
+
+ plusultra.home.extraOptions = {
+ dconf.settings =
+ let
+ user = config.users.users.${config.plusultra.user.name};
+ get-wallpaper = wallpaper:
+ if lib.isDerivation wallpaper
+ then builtins.toString wallpaper
+ else wallpaper;
+ in
+ nested-default-attrs {
+ "org/gnome/shell" = {
+ disable-user-extensions = false;
+ enabled-extensions =
+ (builtins.map (extension: extension.extensionUuid) (cfg.extensions ++ defaultExtensions))
+ ++ [
+ "native-window-placement@gnome-shell-extensions.gcampax.github.com"
+ "drive-menu@gnome-shell-extensions.gcampax.github.com"
+ "user-theme@gnome-shell-extensions.gcampax.github.com"
+ ];
+ favorite-apps =
+ [ "org.gnome.Nautilus.desktop" ]
+ ++ optional config.plusultra.apps.firefox.enable "firefox.desktop"
+ ++ optional config.plusultra.apps.vscode.enable "code.desktop"
+ ++ optional config.plusultra.desktop.addons.foot.enable "foot.desktop"
+ ++ optional config.plusultra.apps.logseq.enable "logseq.desktop"
+ ++ optional config.plusultra.apps.discord.enable "discord.desktop"
+ ++ optional config.plusultra.apps.element.enable "element-desktop.desktop"
+ ++ optional config.plusultra.apps.steam.enable "steam.desktop";
+ };
+
+ "org/gnome/desktop/background" = {
+ picture-uri = get-wallpaper cfg.wallpaper.light;
+ picture-uri-dark = get-wallpaper cfg.wallpaper.dark;
+ };
+ "org/gnome/desktop/screensaver" = {
+ picture-uri = get-wallpaper cfg.wallpaper.light;
+ picture-uri-dark = get-wallpaper cfg.wallpaper.dark;
+ };
+ "org/gnome/desktop/interface" = {
+ color-scheme =
+ if cfg.color-scheme == "light"
+ then "default"
+ else "prefer-dark";
+ enable-hot-corners = false;
+ };
+ "org/gnome/desktop/peripherals/touchpad" = {
+ disable-while-typing = false;
+ };
+ "org/gnome/desktop/wm/preferences" = {
+ num-workspaces = 10;
+ resize-with-right-button = true;
+ };
+ "org/gnome/desktop/wm/keybindings" = {
+ switch-to-workspace-1 = [ "1" ];
+ switch-to-workspace-2 = [ "2" ];
+ switch-to-workspace-3 = [ "3" ];
+ switch-to-workspace-4 = [ "4" ];
+ switch-to-workspace-5 = [ "5" ];
+ switch-to-workspace-6 = [ "6" ];
+ switch-to-workspace-7 = [ "7" ];
+ switch-to-workspace-8 = [ "8" ];
+ switch-to-workspace-9 = [ "9" ];
+ switch-to-workspace-10 = [ "0" ];
+
+ move-to-workspace-1 = [ "1" ];
+ move-to-workspace-2 = [ "2" ];
+ move-to-workspace-3 = [ "3" ];
+ move-to-workspace-4 = [ "4" ];
+ move-to-workspace-5 = [ "5" ];
+ move-to-workspace-6 = [ "6" ];
+ move-to-workspace-7 = [ "7" ];
+ move-to-workspace-8 = [ "8" ];
+ move-to-workspace-9 = [ "9" ];
+ move-to-workspace-10 = [ "0" ];
+ };
+ "org/gnome/shell/keybindings" = {
+ # Remove the default hotkeys for opening favorited applications.
+ switch-to-application-1 = [ ];
+ switch-to-application-2 = [ ];
+ switch-to-application-3 = [ ];
+ switch-to-application-4 = [ ];
+ switch-to-application-5 = [ ];
+ switch-to-application-6 = [ ];
+ switch-to-application-7 = [ ];
+ switch-to-application-8 = [ ];
+ switch-to-application-9 = [ ];
+ switch-to-application-10 = [ ];
+ };
+ "org/gnome/mutter" = {
+ edge-tiling = false;
+ dynamic-workspaces = false;
+ };
+
+ "org/gnome/shell/extensions/dash-to-dock" = {
+ autohide = true;
+ dock-fixed = false;
+ dock-position = "BOTTOM";
+ pressure-threshold = 200.0;
+ require-pressure-to-show = true;
+ show-favorites = true;
+ hot-keys = false;
+ };
+
+ "org/gnome/shell/extensions/just-perfection" = {
+ panel-size = 48;
+ activities-button = false;
+ };
+
+ "org/gnome/shell/extensions/Logo-menu" = {
+ hide-softwarecentre = true;
+
+ # Use right click to open Activities.
+ menu-button-icon-click-type = 3;
+
+ # Use the NixOS logo.
+ menu-button-icon-image = 23;
+
+ menu-button-terminal =
+ if config.plusultra.desktop.addons.term.enable
+ then lib.getExe config.plusultra.desktop.addons.term.pkg
+ else lib.getExe pkgs.gnome.gnome-terminal;
+ };
+
+ "org/gnome/shell/extensions/aylurs-widgets" = {
+ background-clock = false;
+ battery-bar = false;
+ dash-board = false;
+ date-menu-date-format = "%H:%M %B %d";
+ date-menu-hide-clocks = true;
+ date-menu-hide-system-levels = true;
+ date-menu-hide-user = true;
+
+ # Hide the indincator
+ date-menu-indicator-position = 2;
+
+ media-player = false;
+ media-player-prefer = "firefox";
+ notification-indicator = false;
+ power-menu = false;
+ quick-toggles = false;
+ workspace-indicator = false;
+ };
+
+ "org/gnome/shell/extensions/top-bar-organizer" = {
+ left-box-order = [
+ "menuButton"
+ "activities"
+ "dateMenu"
+ "appMenu"
+ ];
+
+ center-box-order = [
+ "Space Bar"
+ ];
+
+ right-box-order = [
+ "keyboard"
+ "EmojisMenu"
+ "wireless-hid"
+ "drive-menu"
+ "vitalsMenu"
+ "screenRecording"
+ "screenSharing"
+ "dwellClick"
+ "a11y"
+ "quickSettings"
+ ];
+ };
+
+ "org/gnome/shell/extensions/space-bar/shortcuts" = {
+ enable-activate-workspace-shortcuts = false;
+ };
+ "org/gnome/shell/extensions/space-bar/behavior" = {
+ show-empty-workspaces = false;
+ };
+
+ "org/gnome/shell/extensions/gtile" = {
+ show-icon = false;
+ grid-sizes = "8x2,4x2,2x2";
+ };
+ };
+ };
+
+ programs.kdeconnect = {
+ enable = true;
+ package = pkgs.gnomeExtensions.gsconnect;
+ };
+
+ # Open firewall for samba connections to work.
+ networking.firewall.extraCommands = "iptables -t raw -A OUTPUT -p udp -m udp --dport 137 -j CT --helper netbios-ns";
+ };
+}
diff --git a/modules/nixos/desktop/gnome/monitors.xml b/modules/nixos/desktop/gnome/monitors.xml
new file mode 100644
index 0000000..32c4e6b
--- /dev/null
+++ b/modules/nixos/desktop/gnome/monitors.xml
@@ -0,0 +1,31 @@
+
+
+
+ 0
+ 0
+ 1
+ yes
+
+
+ DP-3
+ DEL
+ Dell U4919DW
+ D3TXTY2
+
+
+ 5120
+ 1440
+ 59.976879119873047
+
+
+
+
+
+ HDMI-2
+ AOC
+ 28E850
+ 0x00000000
+
+
+
+
diff --git a/modules/nixos/desktop/sway/background.png b/modules/nixos/desktop/sway/background.png
new file mode 100644
index 0000000..1a5a078
Binary files /dev/null and b/modules/nixos/desktop/sway/background.png differ
diff --git a/modules/nixos/desktop/sway/config b/modules/nixos/desktop/sway/config
new file mode 100644
index 0000000..d38c225
--- /dev/null
+++ b/modules/nixos/desktop/sway/config
@@ -0,0 +1,274 @@
+###########################################
+#░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░#
+#░░█▀█░█░░░█░█░█▀▀░░░█░█░█░░░▀█▀░█▀▄░█▀█░░#
+#░░█▀▀░█░░░█░█░▀▀█░░░█░█░█░░░░█░░█▀▄░█▀█░░#
+#░░▀░░░▀▀▀░▀▀▀░▀▀▀░░░▀▀▀░▀▀▀░░▀░░▀░▀░▀░▀░░#
+#░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░#
+###########################################
+
+
+#########################################
+#░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░#
+#░░█░█░█▀█░█▀▄░▀█▀░█▀█░█▀▄░█░░░█▀▀░█▀▀░░#
+#░░▀▄▀░█▀█░█▀▄░░█░░█▀█░█▀▄░█░░░█▀▀░▀▀█░░#
+#░░░▀░░▀░▀░▀░▀░▀▀▀░▀░▀░▀▀░░▀▀▀░▀▀▀░▀▀▀░░#
+#░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░#
+#########################################
+
+# Modifier key (Mod1 = Alt, Mod4 = Meta)
+set $mod Mod4
+
+# Movement keys
+set $left h
+set $down j
+set $up k
+set $right l
+
+# Terminal
+set $term env @term@
+
+# Swaylock
+set $swaylock swaylock-fancy -f "Hack-Regular-Nerd-Font-Complete"
+
+# Menu
+set $menu-run wofi --show drun --prompt search
+
+# Output names
+set $laptop-screen eDP-1
+# set $monitor-left HDMI-A-1
+# set $monitor-center HDMI-A-2
+
+# Workspace names
+set $workspace1 1
+set $workspace2 2
+set $workspace3 3
+set $workspace4 4
+set $workspace5 5
+set $workspace6 6
+set $workspace7 7
+set $workspace8 8
+set $workspace9 9
+set $workspace10 10
+
+#############################
+#░░░░░░░░░░░░░░░░░░░░░░░░░░░#
+#░░█▀█░█░█░▀█▀░█▀█░█░█░▀█▀░░#
+#░░█░█░█░█░░█░░█▀▀░█░█░░█░░░#
+#░░▀▀▀░▀▀▀░░▀░░▀░░░▀▀▀░░▀░░░#
+#░░░░░░░░░░░░░░░░░░░░░░░░░░░#
+#############################
+
+# Backgrounds
+# output $monitor-left bg /home/short/Photos/wallpapers/left.jpg fill
+# output $monitor-center bg /home/short/Photos/wallpapers/center.jpg fill
+
+# Positioning
+# output $monitor-left resolution 1920x1080 position 0,300
+# output $monitor-center resolution 1920x1080 position 1920,0
+
+###################################################
+#░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░#
+#░░█░█░█▀▀░█░█░░░█▀▄░▀█▀░█▀█░█▀▄░▀█▀░█▀█░█▀▀░█▀▀░░#
+#░░█▀▄░█▀▀░░█░░░░█▀▄░░█░░█░█░█░█░░█░░█░█░█░█░▀▀█░░#
+#░░▀░▀░▀▀▀░░▀░░░░▀▀░░▀▀▀░▀░▀░▀▀░░▀▀▀░▀░▀░▀▀▀░▀▀▀░░#
+#░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░#
+###################################################
+
+# Start a terminal
+bindsym $mod+Return exec $term
+
+# Kill focused window
+bindsym $mod+q kill
+
+# Reload configuration
+bindsym $mod+Shift+r reload
+
+# Exit sway
+bindsym $mod+Shift+e exit
+
+# Lock
+bindsym $mod+Control+l exec $swaylock
+
+# Open run menu
+bindsym $mod+d exec $menu-run
+
+# Drag floating windows while holding down modifier key
+floating_modifier $mod normal
+
+# Sticky windows
+bindsym $mod+Shift+s sticky toggle
+
+# Toggle fullscreen
+bindsym $mod+f fullscreen
+
+# Toggle floating
+bindsym $mod+Shift+space floating toggle
+
+# Toggle floating focus
+bindsym $mod+space focus mode_toggle
+
+# Focus the parent container
+bindsym $mod+a focus parent
+
+# Focus the child container
+bindsym $mod+c focus child
+
+# Split horizontal
+bindsym $mod+b splith
+
+# Split vertical
+bindsym $mod+v splitv
+
+# Move window into scratchpad
+bindsym $mod+Shift+minus move scratchpad
+
+# Get window out of scratchpad
+bindsym $mod+minus scratchpad show
+
+# Change layout style
+bindsym $mod+s layout stacking
+bindsym $mod+w layout tabbed
+bindsym $mod+e layout toggle split
+bindsym $mod+Shift+v layout splitv
+bindsym $mod+Shift+b layout splith
+
+# Moving focus
+bindsym $mod+$left focus left
+bindsym $mod+$down focus down
+bindsym $mod+$up focus up
+bindsym $mod+$right focus right
+
+# Moving windows
+bindsym $mod+Shift+$left move left
+bindsym $mod+Shift+$down move down
+bindsym $mod+Shift+$up move up
+bindsym $mod+Shift+$right move right
+
+# Switching workspace
+bindsym $mod+1 workspace $workspace1
+bindsym $mod+2 workspace $workspace2
+bindsym $mod+3 workspace $workspace3
+bindsym $mod+4 workspace $workspace4
+bindsym $mod+5 workspace $workspace5
+bindsym $mod+6 workspace $workspace6
+bindsym $mod+7 workspace $workspace7
+bindsym $mod+8 workspace $workspace8
+bindsym $mod+9 workspace $workspace9
+bindsym $mod+0 workspace $workspace10
+
+# Moving windows to workspace
+bindsym $mod+Shift+1 move container to workspace $workspace1
+bindsym $mod+Shift+2 move container to workspace $workspace2
+bindsym $mod+Shift+3 move container to workspace $workspace3
+bindsym $mod+Shift+4 move container to workspace $workspace4
+bindsym $mod+Shift+5 move container to workspace $workspace5
+bindsym $mod+Shift+6 move container to workspace $workspace6
+bindsym $mod+Shift+7 move container to workspace $workspace7
+bindsym $mod+Shift+8 move container to workspace $workspace8
+bindsym $mod+Shift+9 move container to workspace $workspace9
+bindsym $mod+Shift+0 move container to workspace $workspace10
+
+# Brightness
+bindsym XF86MonBrightnessUp exec brightnessctl set 5%+
+bindsym XF86MonBrightnessDown exec brightnessctl set 5%-
+
+# Sound
+bindsym XF86AudioMute exec pulsemixer --toggle-mute
+bindsym XF86AudioRaiseVolume exec pulsemixer --change-volume +10
+bindsym XF86AudioLowerVolume exec pulsemixer --change-volume -10
+
+# Media
+bindsym XF86AudioPrev exec playerctl previous
+bindsym XF86AudioPlay exec playerctl play-pause
+bindsym XF86AudioNext exec playerctl next
+
+# Print screen
+bindsym Print exec grimshot --notify save screen - | swappy -f -
+bindsym Shift+Print exec grimshot --notify save area - | swappy -f -
+bindsym Control+Print exec grimshot --notify save window - | swappy -f -
+bindsym $mod+Print exec grimshot --notify copy screen
+bindsym $mod+Shift+Print exec grimshot --notify copy area
+bindsym $mod+Control+Print exec grimshot --notify copy window
+
+# Airplane mode
+# bindsym XF86RFKill exec nmcli radio all $(test $(nmcli -g wifi radio all) == "enabled" && "off" || echo "on")
+
+# Resize mode
+mode "resize" {
+ # Shrink the width
+ bindsym $left resize shrink width 10px
+
+ # Grow the width
+ bindsym $right resize grow width 10px
+
+ # Grow the height
+ bindsym $up resize grow height 10px
+
+ # Shrink the height
+ bindsym $down resize shrink height 10px
+
+ # Return to default mode
+ bindsym Return mode "default"
+ bindsym Escape mode "default"
+}
+
+# Enter resize mode
+bindsym $mod+r mode "resize"
+
+######################
+#░░░░░░░░░░░░░░░░░░░░#
+#░▀█▀░█▀█░█▀█░█░█░▀█▀#
+#░░█░░█░█░█▀▀░█░█░░█░#
+#░▀▀▀░▀░▀░▀░░░▀▀▀░░▀░#
+#░░░░░░░░░░░░░░░░░░░░#
+######################
+
+input type:keyboard {
+ xkb_options caps:swapescape
+}
+
+input type:touchpad {
+ natural_scroll enabled
+ tap enabled
+ tap_button_map lrm
+}
+
+#################
+#░░░░░░░░░░░░░░░#
+#░░█▀▄░█▀█░█▀▄░░#
+#░░█▀▄░█▀█░█▀▄░░#
+#░░▀▀░░▀░▀░▀░▀░░#
+#░░░░░░░░░░░░░░░#
+#################
+
+bar {
+ # Run waybar
+ swaybar_command waybar
+}
+
+#########################
+#░░░░░░░░░░░░░░░░░░░░░░░#
+#░░█▀▀░▀█▀░█░█░█░░░█▀▀░░#
+#░░▀▀█░░█░░░█░░█░░░█▀▀░░#
+#░░▀▀▀░░▀░░░▀░░▀▀▀░▀▀▀░░#
+#░░░░░░░░░░░░░░░░░░░░░░░#
+#########################
+
+# Remove title bars
+default_border pixel 1
+default_floating_border pixel 1
+
+client.focused #8fbcbb #8fbcbb #eceff4 #eceff4
+client.focused_inactive #2E3440 #2E3440 #e5e9f0 #5e81ac
+client.unfocused #3b4252 #3b4252 #4c566a #5e81ac
+client.urgent #d08770 #d08770 #eceff4 #bf616a
+
+for_window [app_id="wofi"] border none
+# for_window [app_id="firefox"] border none
+
+# Don't focus moused over windows
+focus_follows_mouse no
+
+# Gaps
+gaps outer 4
+gaps inner 5
diff --git a/modules/nixos/desktop/sway/default.nix b/modules/nixos/desktop/sway/default.nix
new file mode 100644
index 0000000..d06f9ec
--- /dev/null
+++ b/modules/nixos/desktop/sway/default.nix
@@ -0,0 +1,148 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.desktop.sway;
+ term = config.plusultra.desktop.addons.term;
+ substitutedConfig = pkgs.substituteAll {
+ src = ./config;
+ term = term.pkg.pname or term.pkg.name;
+ };
+in
+{
+ options.plusultra.desktop.sway = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Sway.";
+ wallpaper = mkOpt (nullOr package) null "The wallpaper to display.";
+ extraConfig =
+ mkOpt str "" "Additional configuration for the Sway config file.";
+ };
+
+ config = mkIf cfg.enable {
+ # Desktop additions
+ plusultra.desktop.addons = {
+ gtk = enabled;
+ foot = enabled;
+ mako = enabled;
+ rofi = enabled;
+ wofi = enabled;
+ swappy = enabled;
+ kanshi = enabled;
+ waybar = enabled;
+ keyring = enabled;
+ nautilus = enabled;
+ xdg-portal = enabled;
+ electron-support = enabled;
+ };
+
+ plusultra.home.configFile."sway/config".text =
+ fileWithText substitutedConfig ''
+ #############################
+ #░░░░░░░░░░░░░░░░░░░░░░░░░░░#
+ #░░█▀▀░█░█░█▀▀░▀█▀░█▀▀░█▄█░░#
+ #░░▀▀█░░█░░▀▀█░░█░░█▀▀░█░█░░#
+ #░░▀▀▀░░▀░░▀▀▀░░▀░░▀▀▀░▀░▀░░#
+ #░░░░░░░░░░░░░░░░░░░░░░░░░░░#
+ #############################
+
+ # Launch services waiting for the systemd target sway-session.target
+ exec "systemctl --user import-environment; systemctl --user start sway-session.target"
+
+ # Start a user session dbus (required for things like starting
+ # applications through wofi).
+ exec dbus-daemon --session --address=unix:path=$XDG_RUNTIME_DIR/bus
+
+ ${optionalString (cfg.wallpaper != null) ''
+ output * {
+ bg ${cfg.wallpaper.gnomeFilePath or cfg.wallpaper} fill
+ }
+ ''}
+
+ ${cfg.extraConfig}
+ '';
+
+ programs.sway = {
+ enable = true;
+ extraPackages = with pkgs; [
+ rofi
+ swaylock
+ swayidle
+ xwayland
+ sway-contrib.grimshot
+ swaylock-fancy
+ wl-clipboard
+ wf-recorder
+ libinput
+ playerctl
+ brightnessctl
+ glib # for gsettings
+ gtk3.out # for gtk-launch
+ gnome.gnome-control-center
+ ];
+
+ extraSessionCommands = ''
+ export SDL_VIDEODRIVER=wayland
+ export QT_QPA_PLATFORM=wayland
+ export QT_WAYLAND_DISABLE_WINDOWDECORATION="1"
+ export _JAVA_AWT_WM_NONREPARENTING=1
+ export MOZ_ENABLE_WAYLAND=1
+ export XDG_SESSION_TYPE=wayland
+ export XDG_SESSION_DESKTOP=sway
+ export XDG_CURRENT_DESKTOP=sway
+ '';
+ };
+
+ environment.systemPackages = with pkgs;
+ [
+ (pkgs.writeTextFile {
+ name = "startsway";
+ destination = "/bin/startsway";
+ executable = true;
+ text = ''
+ #! ${pkgs.bash}/bin/bash
+
+ # Import environment variables from the login manager
+ systemctl --user import-environment
+
+ # Start Sway
+ exec systemctl --user start sway.service
+ '';
+ })
+ ];
+
+ # configuring sway itself (assmung a display manager starts it)
+ systemd.user.targets.sway-session = {
+ description = "Sway compositor session";
+ documentation = [ "man:systemd.special(7)" ];
+ bindsTo = [ "graphical-session.target" ];
+ wants = [ "graphical-session-pre.target" ];
+ after = [ "graphical-session-pre.target" ];
+ };
+
+ systemd.user.services.sway = {
+ description = "Sway - Wayland window manager";
+ documentation = [ "man:sway(5)" ];
+ bindsTo = [ "graphical-session.target" ];
+ wants = [ "graphical-session-pre.target" ];
+ after = [ "graphical-session-pre.target" ];
+ # We explicitly unset PATH here, as we want it to be set by
+ # systemctl --user import-environment in startsway
+ environment.PATH = lib.mkForce null;
+ serviceConfig = {
+ Type = "simple";
+ ExecStart = ''
+ ${pkgs.dbus}/bin/dbus-run-session ${pkgs.sway}/bin/sway --debug
+ '';
+ Restart = "on-failure";
+ RestartSec = 1;
+ TimeoutStopSec = 10;
+ };
+ };
+
+ services.xserver.enable = true;
+ services.xserver.displayManager.defaultSession = "sway";
+ services.xserver.displayManager.gdm.enable = true;
+ services.xserver.displayManager.gdm.wayland = true;
+ services.xserver.libinput.enable = true;
+ };
+}
diff --git a/modules/nixos/hardware/audio/default.nix b/modules/nixos/hardware/audio/default.nix
new file mode 100644
index 0000000..001e463
--- /dev/null
+++ b/modules/nixos/hardware/audio/default.nix
@@ -0,0 +1,200 @@
+{ options
+, config
+, pkgs
+, lib
+, ...
+}:
+# FIXME: The transition to wireplumber from media-session has completely
+# broken my setup. I'll need to invest some time to figure out how to override Alsa things
+# again...
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.hardware.audio;
+
+ lua-format = {
+ type = with lib.types; let
+ valueType =
+ nullOr
+ (oneOf [
+ bool
+ int
+ float
+ str
+ path
+ (attrsOf valueType)
+ (listOf valueType)
+ ])
+ // {
+ description = "Lua value";
+ };
+ in
+ valueType;
+
+ generate = name: value:
+ let
+ toLuaValue = value:
+ if value == null
+ then "null"
+ else if value == true
+ then "true"
+ else if value == false
+ then "false"
+ else if builtins.isInt value || builtins.isFloat value
+ then builtins.toString value
+ else if builtins.isString value
+ then toLuaString value
+ else if builtins.isAttrs value
+ then toLuaTable value
+ else if builtins.isList value
+ then toLuaList value
+ else builtins.abort "Unsupported value used with formats.lua.generate: ${value}";
+
+ toLuaString = value: "\"${builtins.toString value}\"";
+
+ toLuaTable = value:
+ let
+ pairs =
+ mapAttrsToList
+ (name: value: "[${toLuaString name}] = ${toLuaValue value}")
+ value;
+ content = concatStringsSep ", " pairs;
+ in
+ "{ ${content} }";
+
+ toLuaList = value:
+ let
+ parts = builtins.map toLuaValue value;
+ content = concatStringsSep ", " parts;
+ in
+ "{ ${content} }";
+ in
+ toLuaValue value;
+ };
+
+ pipewire-config = {
+ "context.objects" = cfg.nodes ++ [ ];
+ "context.modules" =
+ [
+ {
+ name = "libpipewire-module-rtkit";
+ args = { };
+ flags = [ "ifexists" "nofail" ];
+ }
+ { name = "libpipewire-module-protocol-native"; }
+ { name = "libpipewire-module-profiler"; }
+ # {
+ # name = "libpipewire-module-metadata";
+ # flags = [ "ifexists" "nofail" ];
+ # }
+ { name = "libpipewire-module-spa-device-factory"; }
+ { name = "libpipewire-module-spa-node-factory"; }
+ # {
+ # name = "libpipewire-module-client-node";
+ # flags = [ "ifexists" "nofail" ];
+ # }
+ # {
+ # name = "libpipewire-module-client-device";
+ # flags = [ "ifexists" "nofail" ];
+ # }
+ {
+ name = "libpipewire-module-portal";
+ flags = [ "ifexists" "nofail" ];
+ }
+ {
+ name = "libpipewire-module-access";
+ args = { };
+ }
+ { name = "libpipewire-module-adapter"; }
+ { name = "libpipewire-module-link-factory"; }
+ { name = "libpipewire-module-session-manager"; }
+ ]
+ ++ cfg.modules;
+ "context.components" = [
+ {
+ name = "libwireplumber-module-lua-scripting";
+ type = "module";
+ }
+ {
+ name = "config.lua";
+ type = "config/lua";
+ }
+ ];
+ };
+
+ alsa-config = {
+ alsa_monitor = cfg.alsa-monitor;
+ };
+in
+{
+ options.plusultra.hardware.audio = with types; {
+ enable = mkBoolOpt false "Whether or not to enable audio support.";
+ alsa-monitor = mkOpt attrs { } "Alsa configuration.";
+ nodes =
+ mkOpt (listOf attrs) [ ]
+ "Audio nodes to pass to Pipewire as `context.objects`.";
+ modules =
+ mkOpt (listOf attrs) [ ]
+ "Audio modules to pass to Pipewire as `context.modules`.";
+ extra-packages = mkOpt (listOf package) [
+ pkgs.qjackctl
+ pkgs.easyeffects
+ ] "Additional packages to install.";
+ };
+
+ config = mkIf cfg.enable {
+ security.rtkit.enable = true;
+
+ services.pipewire = {
+ enable = true;
+ alsa.enable = true;
+ pulse.enable = true;
+ jack.enable = true;
+
+ wireplumber.enable = true;
+ };
+
+ environment.etc = {
+ # "pipewire/pipewire.conf.d/10-pipewire.conf".source =
+ # pkgs.writeText "pipewire.conf" (builtins.toJSON pipewire-config);
+ # "pipewire/pipewire.conf.d/21-alsa.conf".source =
+ # pkgs.writeText "pipewire.conf" (builtins.toJSON alsa-config);
+
+ # "wireplumber/wireplumber.conf".source =
+ # pkgs.writeText "pipewire.conf" (builtins.toJSON pipewire-config);
+
+ # "wireplumber/scripts/config.lua.d/alsa.lua".text = ''
+ # local input = ${lua-format.generate "sample.lua" cfg.alsa-monitor}
+
+ # if input.rules == nil then
+ # input.rules = {}
+ # end
+
+ # local rules = input.rules
+
+ # for _, rule in ipairs(input.rules) do
+ # table.insert(alsa_monitor.rules, rule)
+ # end
+ # '';
+ };
+
+ hardware.pulseaudio.enable = mkForce false;
+
+ environment.systemPackages = with pkgs;
+ [
+ pulsemixer
+ pavucontrol
+ ]
+ ++ cfg.extra-packages;
+
+ plusultra.user.extraGroups = [ "audio" ];
+
+ plusultra.home.extraOptions = {
+ systemd.user.services.mpris-proxy = {
+ Unit.Description = "Mpris proxy";
+ Unit.After = [ "network.target" "sound.target" ];
+ Service.ExecStart = "${pkgs.bluez}/bin/mpris-proxy";
+ Install.WantedBy = [ "default.target" ];
+ };
+ };
+ };
+}
diff --git a/modules/nixos/hardware/fingerprint/default.nix b/modules/nixos/hardware/fingerprint/default.nix
new file mode 100644
index 0000000..5c6dbf8
--- /dev/null
+++ b/modules/nixos/hardware/fingerprint/default.nix
@@ -0,0 +1,13 @@
+{ options, config, pkgs, lib, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.hardware.fingerprint;
+in
+{
+ options.plusultra.hardware.fingerprint = with types; {
+ enable = mkBoolOpt false "Whether or not to enable fingerprint support.";
+ };
+
+ config = mkIf cfg.enable { services.fprintd.enable = true; };
+}
diff --git a/modules/nixos/hardware/networking/default.nix b/modules/nixos/hardware/networking/default.nix
new file mode 100644
index 0000000..3cec0a8
--- /dev/null
+++ b/modules/nixos/hardware/networking/default.nix
@@ -0,0 +1,32 @@
+{ options, config, pkgs, lib, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.hardware.networking;
+in
+{
+ options.plusultra.hardware.networking = with types; {
+ enable = mkBoolOpt false "Whether or not to enable networking support";
+ hosts = mkOpt attrs { }
+ (mdDoc "An attribute set to merge with `networking.hosts`");
+ };
+
+ config = mkIf cfg.enable {
+ plusultra.user.extraGroups = [ "networkmanager" ];
+
+ networking = {
+ hosts = {
+ "127.0.0.1" = [ "local.test" ] ++ (cfg.hosts."127.0.0.1" or [ ]);
+ } // cfg.hosts;
+
+ networkmanager = {
+ enable = true;
+ dhcp = "internal";
+ };
+ };
+
+ # Fixes an issue that normally causes nixos-rebuild to fail.
+ # https://github.com/NixOS/nixpkgs/issues/180175
+ systemd.services.NetworkManager-wait-online.enable = false;
+ };
+}
diff --git a/modules/nixos/hardware/storage/default.nix b/modules/nixos/hardware/storage/default.nix
new file mode 100644
index 0000000..2f07e62
--- /dev/null
+++ b/modules/nixos/hardware/storage/default.nix
@@ -0,0 +1,16 @@
+{ options, config, pkgs, lib, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.hardware.storage;
+in
+{
+ options.plusultra.hardware.storage = with types; {
+ enable = mkBoolOpt false
+ "Whether or not to enable support for extra storage devices.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [ ntfs3g fuseiso ];
+ };
+}
diff --git a/modules/nixos/home/default.nix b/modules/nixos/home/default.nix
new file mode 100644
index 0000000..3f47f81
--- /dev/null
+++ b/modules/nixos/home/default.nix
@@ -0,0 +1,36 @@
+{ options, config, pkgs, lib, inputs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.home;
+in
+{
+ # imports = with inputs; [
+ # home-manager.nixosModules.home-manager
+ # ];
+
+ options.plusultra.home = with types; {
+ file = mkOpt attrs { }
+ (mdDoc "A set of files to be managed by home-manager's `home.file`.");
+ configFile = mkOpt attrs { }
+ (mdDoc "A set of files to be managed by home-manager's `xdg.configFile`.");
+ extraOptions = mkOpt attrs { } "Options to pass directly to home-manager.";
+ };
+
+ config = {
+ plusultra.home.extraOptions = {
+ home.stateVersion = config.system.stateVersion;
+ home.file = mkAliasDefinitions options.plusultra.home.file;
+ xdg.enable = true;
+ xdg.configFile = mkAliasDefinitions options.plusultra.home.configFile;
+ };
+
+ home-manager = {
+ useUserPackages = true;
+ useGlobalPkgs = true;
+
+ users.${config.plusultra.user.name} =
+ mkAliasDefinitions options.plusultra.home.extraOptions;
+ };
+ };
+}
diff --git a/modules/nixos/nix/default.nix b/modules/nixos/nix/default.nix
new file mode 100644
index 0000000..ec2a79c
--- /dev/null
+++ b/modules/nixos/nix/default.nix
@@ -0,0 +1,92 @@
+{ options, config, pkgs, lib, inputs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.nix;
+
+ substituters-submodule = types.submodule ({ name, ... }: {
+ options = with types; {
+ key = mkOpt (nullOr str) null "The trusted public key for this substituter.";
+ };
+ });
+in
+{
+ options.plusultra.nix = with types; {
+ enable = mkBoolOpt true "Whether or not to manage nix configuration.";
+ package = mkOpt package pkgs.nixUnstable "Which nix package to use.";
+
+ default-substituter = {
+ url = mkOpt str "https://cache.nixos.org" "The url for the substituter.";
+ key = mkOpt str "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" "The trusted public key for the substituter.";
+ };
+
+ extra-substituters = mkOpt (attrsOf substituters-submodule) { } "Extra substituters to configure.";
+ };
+
+ config = mkIf cfg.enable {
+ assertions = mapAttrsToList
+ (name: value: {
+ assertion = value.key != null;
+ message = "plusultra.nix.extra-substituters.${name}.key must be set";
+ })
+ cfg.extra-substituters;
+
+ environment.systemPackages = with pkgs; [
+ plusultra.nixos-revision
+ (plusultra.nixos-hosts.override {
+ hosts = inputs.self.nixosConfigurations;
+ })
+ deploy-rs
+ nixfmt
+ nix-index
+ nix-prefetch-git
+ nix-output-monitor
+ flake-checker
+ ];
+
+ nix =
+ let
+ users = [ "root" config.plusultra.user.name ] ++
+ optional config.services.hydra.enable "hydra";
+ in
+ {
+ package = cfg.package;
+
+ settings = {
+ experimental-features = "nix-command flakes";
+ http-connections = 50;
+ warn-dirty = false;
+ log-lines = 50;
+ sandbox = "relaxed";
+ auto-optimise-store = true;
+ trusted-users = users;
+ allowed-users = users;
+
+ substituters =
+ [ cfg.default-substituter.url ]
+ ++
+ (mapAttrsToList (name: value: name) cfg.extra-substituters);
+ trusted-public-keys =
+ [ cfg.default-substituter.key ]
+ ++
+ (mapAttrsToList (name: value: value.key) cfg.extra-substituters);
+
+ } // (lib.optionalAttrs config.plusultra.tools.direnv.enable {
+ keep-outputs = true;
+ keep-derivations = true;
+ });
+
+ gc = {
+ automatic = true;
+ dates = "weekly";
+ options = "--delete-older-than 30d";
+ };
+
+ # flake-utils-plus
+ generateRegistryFromInputs = true;
+ generateNixPathFromInputs = true;
+ linkInputs = true;
+ };
+ };
+}
diff --git a/modules/nixos/security/acme/default.nix b/modules/nixos/security/acme/default.nix
new file mode 100644
index 0000000..dc2b258
--- /dev/null
+++ b/modules/nixos/security/acme/default.nix
@@ -0,0 +1,31 @@
+{ lib, pkgs, config, virtual, ... }:
+
+let
+ inherit (lib) mkIf mkEnableOption optional;
+ inherit (lib.plusultra) mkOpt;
+
+ cfg = config.plusultra.security.acme;
+in
+{
+ options.plusultra.security.acme = with lib.types; {
+ enable = mkEnableOption "default ACME configuration";
+ email = mkOpt str config.plusultra.user.email "The email to use.";
+ staging = mkOpt bool virtual "Whether to use the staging server or not.";
+ };
+
+ config = mkIf cfg.enable {
+ security.acme = {
+ acceptTerms = true;
+
+ defaults = {
+ inherit (cfg) email;
+
+ group = mkIf config.services.nginx.enable "nginx";
+ server = mkIf cfg.staging "https://acme-staging-v02.api.letsencrypt.org/directory";
+
+ # Reload nginx when certs change.
+ reloadServices = optional config.services.nginx.enable "nginx.service";
+ };
+ };
+ };
+}
diff --git a/modules/nixos/security/doas/default.nix b/modules/nixos/security/doas/default.nix
new file mode 100644
index 0000000..a586858
--- /dev/null
+++ b/modules/nixos/security/doas/default.nix
@@ -0,0 +1,29 @@
+{ options, config, pkgs, lib, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.security.doas;
+in
+{
+ options.plusultra.security.doas = {
+ enable = mkBoolOpt false "Whether or not to replace sudo with doas.";
+ };
+
+ config = mkIf cfg.enable {
+ # Disable sudo
+ security.sudo.enable = false;
+
+ # Enable and configure `doas`.
+ security.doas = {
+ enable = true;
+ extraRules = [{
+ users = [ config.plusultra.user.name ];
+ noPass = true;
+ keepEnv = true;
+ }];
+ };
+
+ # Add an alias to the shell for backward-compat and convenience.
+ environment.shellAliases = { sudo = "doas"; };
+ };
+}
diff --git a/modules/nixos/security/gpg/default.nix b/modules/nixos/security/gpg/default.nix
new file mode 100644
index 0000000..fccfbc1
--- /dev/null
+++ b/modules/nixos/security/gpg/default.nix
@@ -0,0 +1,118 @@
+{ options
+, config
+, pkgs
+, lib
+, inputs
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.security.gpg;
+
+ gpgConf = "${inputs.gpg-base-conf}/gpg.conf";
+
+ gpgAgentConf = ''
+ enable-ssh-support
+ default-cache-ttl 60
+ max-cache-ttl 120
+ pinentry-program ${pkgs.pinentry-gnome}/bin/pinentry-gnome3
+ '';
+
+ guide = "${inputs.yubikey-guide}/README.md";
+
+ theme = pkgs.fetchFromGitHub {
+ owner = "jez";
+ repo = "pandoc-markdown-css-theme";
+ rev = "019a4829242937761949274916022e9861ed0627";
+ sha256 = "1h48yqffpaz437f3c9hfryf23r95rr319lrb3y79kxpxbc9hihxb";
+ };
+
+ guideHTML = pkgs.runCommand "yubikey-guide" { } ''
+ ${pkgs.pandoc}/bin/pandoc \
+ --standalone \
+ --metadata title="Yubikey Guide" \
+ --from markdown \
+ --to html5+smart \
+ --toc \
+ --template ${theme}/template.html5 \
+ --css ${theme}/docs/css/theme.css \
+ --css ${theme}/docs/css/skylighting-solarized-theme.css \
+ -o $out \
+ ${guide}
+ '';
+
+ guideDesktopItem = pkgs.makeDesktopItem {
+ name = "yubikey-guide";
+ desktopName = "Yubikey Guide";
+ genericName = "View Yubikey Guide in a web browser";
+ exec = "${pkgs.xdg-utils}/bin/xdg-open ${guideHTML}";
+ icon = ./yubico-icon.svg;
+ categories = [ "System" ];
+ };
+
+ reload-yubikey = pkgs.writeShellScriptBin "reload-yubikey" ''
+ ${pkgs.gnupg}/bin/gpg-connect-agent "scd serialno" "learn --force" /bye
+ '';
+in
+{
+ options.plusultra.security.gpg = with types; {
+ enable = mkBoolOpt false "Whether or not to enable GPG.";
+ agentTimeout = mkOpt int 5 "The amount of time to wait before continuing with shell init.";
+ };
+
+ config = mkIf cfg.enable {
+ services.pcscd.enable = true;
+ services.udev.packages = with pkgs; [ yubikey-personalization ];
+
+ # NOTE: This should already have been added by programs.gpg, but
+ # keeping it here for now just in case.
+ environment.shellInit = ''
+ export GPG_TTY="$(tty)"
+ export SSH_AUTH_SOCK=$(${pkgs.gnupg}/bin/gpgconf --list-dirs agent-ssh-socket)
+
+ ${pkgs.coreutils}/bin/timeout ${builtins.toString cfg.agentTimeout} ${pkgs.gnupg}/bin/gpgconf --launch gpg-agent
+ gpg_agent_timeout_status=$?
+
+ if [ "$gpg_agent_timeout_status" = 124 ]; then
+ # Command timed out...
+ echo "GPG Agent timed out..."
+ echo 'Run "gpgconf --launch gpg-agent" to try and launch it again.'
+ fi
+ '';
+
+ environment.systemPackages = with pkgs; [
+ cryptsetup
+ paperkey
+ gnupg
+ pinentry-curses
+ pinentry-qt
+ pinentry-gnome
+ paperkey
+ guideDesktopItem
+ reload-yubikey
+ ];
+
+ programs = {
+ ssh.startAgent = false;
+
+ gnupg.agent = {
+ enable = true;
+ enableSSHSupport = true;
+ enableExtraSocket = true;
+ pinentryFlavor = "gnome3";
+ };
+ };
+
+ plusultra = {
+ home.file = {
+ ".gnupg/.keep".text = "";
+
+ ".gnupg/yubikey-guide.md".source = guide;
+ ".gnupg/yubikey-guide.html".source = guideHTML;
+
+ ".gnupg/gpg.conf".source = gpgConf;
+ ".gnupg/gpg-agent.conf".text = gpgAgentConf;
+ };
+ };
+ };
+}
diff --git a/modules/nixos/security/gpg/yubico-icon.svg b/modules/nixos/security/gpg/yubico-icon.svg
new file mode 100644
index 0000000..ebfa48c
--- /dev/null
+++ b/modules/nixos/security/gpg/yubico-icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/modules/nixos/security/keyring/default.nix b/modules/nixos/security/keyring/default.nix
new file mode 100644
index 0000000..00ea04d
--- /dev/null
+++ b/modules/nixos/security/keyring/default.nix
@@ -0,0 +1,19 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.security.keyring;
+in
+{
+ options.plusultra.security.keyring = with types; {
+ enable = mkBoolOpt false "Whether to enable gnome keyring.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [
+ gnome.gnome-keyring
+ gnome.libgnome-keyring
+ ];
+ };
+}
diff --git a/modules/nixos/services/attic/default.nix b/modules/nixos/services/attic/default.nix
new file mode 100644
index 0000000..a008fe2
--- /dev/null
+++ b/modules/nixos/services/attic/default.nix
@@ -0,0 +1,97 @@
+{ lib, config, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.services.attic;
+
+ toml-format = pkgs.formats.toml { };
+
+ raw-server-toml = toml-format.generate "server.toml" cfg.settings;
+
+ server-toml = pkgs.runCommand "checked-server.toml" { config = raw-server-toml; } ''
+ cat $config
+
+ export ATTIC_SERVER_TOKEN_HS256_SECRET_BASE64="dGVzdCBzZWNyZXQ="
+ export ATTIC_SERVER_DATABASE_URL="sqlite://:memory:"
+
+ ${cfg.package}/bin/atticd --mode check-config -f "$config"
+
+ cat < $config > $out
+ '';
+
+ is-local-postgres =
+ let
+ url = cfg.settings.database.url or "";
+ local-db-strings = [ "localhost" "127.0.0.1" "/run/postgresql" ];
+ is-local-db-url = any (flip hasInfix url) local-db-strings;
+ in
+ config.services.postgresql.enable
+ && hasPrefix "postgresql://" url
+ && is-local-db-url;
+in
+{
+ options.plusultra.services.attic = {
+ enable = mkEnableOption "Attic";
+
+ package = mkOpt types.package pkgs.attic-server "The attic-server package to use.";
+
+ credentials = mkOpt (types.nullOr types.path) null "The path to an optional EnvironmentFile for the atticd service to use.";
+
+ user = mkOpt types.str "atticd" "The user under which attic runs.";
+ group = mkOpt types.str "atticd" "The group under which attic runs.";
+
+ settings = mkOpt toml-format.type { } "Settings for the atticd config file.";
+ };
+
+ config = mkIf cfg.enable {
+ assertions = [
+ {
+ assertion = !isStorePath cfg.credentials;
+ message = "plusultra.services.attic.credentials CANNOT be in the Nix Store.";
+ }
+ ];
+
+ users = {
+ users = optionalAttrs (cfg.user == "atticd") {
+ atticd = {
+ group = cfg.group;
+ isSystemUser = true;
+ };
+ };
+
+ groups = optionalAttrs (cfg.group == "atticd") {
+ atticd = { };
+ };
+ };
+
+ plusultra = {
+ tools.attic = enabled;
+
+ services.attic.settings = {
+ database.url = mkDefault "sqlite:///var/lib/atticd/server.db?mode=rwc";
+
+ storage = mkDefault {
+ type = "local";
+ path = "/var/lib/atticd/storage";
+ };
+ };
+ };
+
+ systemd.services.atticd = {
+ wantedBy = [ "multi-user.target" ];
+ after = [ "network.target" ]
+ ++ optionals is-local-postgres [ "postgresql.service" "nss-lookup.target" ];
+
+ serviceConfig = {
+ ExecStart = "${cfg.package}/bin/atticd -f ${server-toml}";
+ StateDirectory = "atticd";
+ User = cfg.user;
+ Group = cfg.group;
+ DynamicUser = true;
+ } // optionalAttrs (cfg.credentials != null) {
+ EnvironmentFile = mkDefault cfg.credentials;
+ };
+ };
+ };
+}
diff --git a/modules/nixos/services/avahi/default.nix b/modules/nixos/services/avahi/default.nix
new file mode 100644
index 0000000..175316f
--- /dev/null
+++ b/modules/nixos/services/avahi/default.nix
@@ -0,0 +1,41 @@
+{ lib, config, options, ... }:
+
+let
+ cfg = config.plusultra.services.avahi;
+
+ inherit (lib) types mkEnableOption mkIf;
+in
+{
+ options.plusultra.services.avahi = with types; {
+ enable = mkEnableOption "Avahi";
+ };
+
+ config = mkIf cfg.enable {
+ services.avahi = {
+ enable = true;
+ nssmdns = true;
+ publish = {
+ enable = true;
+ addresses = true;
+ domain = true;
+ hinfo = true;
+ userServices = true;
+ workstation = true;
+ };
+
+ extraServiceFiles = {
+ smb = ''
+
+
+
+ %h
+
+ _smb._tcp
+ 445
+
+
+ '';
+ };
+ };
+ };
+}
diff --git a/modules/nixos/services/cowsay-mastodon-poster/default.nix b/modules/nixos/services/cowsay-mastodon-poster/default.nix
new file mode 100644
index 0000000..17ec466
--- /dev/null
+++ b/modules/nixos/services/cowsay-mastodon-poster/default.nix
@@ -0,0 +1,66 @@
+{ lib, pkgs, config, ... }:
+
+let
+ inherit (lib) types mkIf;
+ inherit (lib.plusultra) mkBoolOpt mkOpt;
+ inherit (pkgs) fortune toot;
+ inherit (pkgs.snowfallorg) cow2img;
+
+ cfg = config.plusultra.services.cowsay-mastodon-poster;
+
+ script = ''
+ if [ ! -f ~/.config/toot/config.json ]; then
+ echo "File ~/.config/toot/config.json does not exist. Run 'toot login_cli' first."
+ exit 1
+ fi
+
+ tmp_dir=$(mktemp -d)
+
+ pushd $tmp_dir > /dev/null
+ ${cow2img}/bin/cow2img --no-spinner ${if cfg.short then "--message \"$(${fortune}/bin/fortune -s)\"" else ""}
+
+ cow_name=$(cat ./cow/name)
+ cow_message=$(cat ./cow/message)
+
+ post="$cow_name saying:"$'\n\n'"$cow_message"
+
+ ${toot}/bin/toot post --media ./cow/image.png --description "$post" "#hachybots"
+ popd > /dev/null
+
+ rm -rf $tmp_dir
+ '';
+in
+{
+ options.plusultra.services.cowsay-mastodon-poster = with types; {
+ enable = mkBoolOpt false "Whether or not to enable cowsay posts.";
+ short = mkBoolOpt false "Use short fortunes only.";
+ user = mkOpt str config.plusultra.user.name "The user to run as.";
+ group = mkOpt str "users" "The group to run as.";
+ };
+
+ config = mkIf cfg.enable {
+ systemd = {
+ timers.cowsay-mastodon-poster = {
+ wantedBy = [ "timers.target" ];
+ timerConfig = {
+ # Run once a day at 10am.
+ OnCalendar = "*-*-* 10:00:00";
+ Unit = "cowsay-mastodon-poster.service";
+ };
+ };
+
+ services.cowsay-mastodon-poster = {
+ after = [ "network-online.target" ];
+ description = "Post a cowsay image to Mastodon.";
+
+ inherit script;
+
+ serviceConfig = {
+ Type = "oneshot";
+ User = cfg.user;
+ Group = cfg.group;
+ };
+ };
+ };
+ };
+}
diff --git a/modules/nixos/services/dex/default.nix b/modules/nixos/services/dex/default.nix
new file mode 100644
index 0000000..f37253f
--- /dev/null
+++ b/modules/nixos/services/dex/default.nix
@@ -0,0 +1,198 @@
+{ config, lib, pkgs, ... }:
+
+let
+ inherit (builtins) map removeAttrs;
+ inherit (lib) mapAttrs flatten concatMap concatMapStringsSep;
+
+ cfg = config.plusultra.services.dex;
+
+ process-client-settings = client:
+ if client ? secretFile then
+ (removeAttrs client [ "secretFile" ])
+ // { secret = client.secretFile; }
+ else
+ client;
+
+ settings =
+ mapAttrs
+ (name: value:
+ if name == "staticClients" then
+ map process-client-settings value
+ else
+ value
+ )
+ (cfg.settings // {
+ storage = (cfg.settings.storage or { }) // {
+ type = cfg.settings.storage.type or "sqlite3";
+ config = cfg.settings.storage.config or {
+ file = "${cfg.stateDir}/dex.db";
+ };
+ };
+ });
+
+ secret-files = concatMap
+ (client:
+ if client ? secretFile then
+ [ client.secretFile ]
+ else
+ [ ]
+ )
+ (settings.staticClients or [ ]);
+
+ format = pkgs.formats.yaml { };
+
+ configYaml = format.generate "config.yaml" settings;
+
+ replace-config-secrets = pkgs.writeShellScript "replace-config-secrets"
+ (concatMapStringsSep "\n"
+ (file: "${pkgs.replace-secret}/bin/replace-secret '${file}' '${file}' ${cfg.stateDir}/config.yaml")
+ secret-files
+ );
+
+in
+{
+ options.plusultra.services.dex = {
+ enable = lib.mkEnableOption "Dex, the OpenID Connect and OAuth 2 identity provider";
+
+ stateDir = lib.mkOption {
+ type = lib.types.path;
+ default = "/var/lib/dex";
+ description = "The state directory where config and data are stored.";
+ };
+
+ user = lib.mkOption {
+ type = lib.types.str;
+ default = "dex";
+ description = "The user to run Dex as.";
+ };
+
+ group = lib.mkOption {
+ type = lib.types.str;
+ default = "dex";
+ description = "The group to run Dex as.";
+ };
+
+ settings = lib.mkOption {
+ type = format.type;
+ default = { };
+ example = lib.literalExpression ''
+ {
+ # External url
+ issuer = "http://127.0.0.1:5556/dex";
+ storage = {
+ type = "postgres";
+ config.host = "/var/run/postgres";
+ };
+ web = {
+ http = "127.0.0.1:5556";
+ };
+ enablePasswordDB = true;
+ staticClients = [
+ {
+ id = "oidcclient";
+ name = "Client";
+ redirectURIs = [ "https://example.com/callback" ];
+
+ # The content of `secretFile` will be written into to the config as `secret`.
+ secretFile = "/etc/dex/oidcclient";
+ }
+ ];
+ }
+ '';
+ description = lib.mdDoc ''
+ The available options can be found in
+ [the example configuration](https://github.com/dexidp/dex/blob/v${pkgs.dex.version}/config.yaml.dist).
+ '';
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ users = {
+ users = lib.optionalAttrs (cfg.user == "dex") {
+ dex = {
+ group = cfg.group;
+ home = cfg.stateDir;
+ isSystemUser = true;
+ };
+ };
+
+ groups = lib.optionalAttrs (cfg.group == "dex") {
+ dex = { };
+ };
+ };
+
+ systemd = {
+ tmpfiles.rules = [
+ "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group}"
+ ];
+
+ services = {
+ dex = {
+ description = "dex identity provider";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "networking.target" ];
+
+ preStart = ''
+ cp --remove-destination ${configYaml} ${cfg.stateDir}/config.yaml
+
+ chmod 600 ${cfg.stateDir}/config.yaml
+
+ ${replace-config-secrets}
+ '';
+
+ serviceConfig = {
+ ExecStart = "${pkgs.dex-oidc}/bin/dex serve ${cfg.stateDir}/config.yaml";
+ WorkingDirectory = cfg.stateDir;
+
+ User = cfg.user;
+ Group = cfg.group;
+
+ AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+ BindReadOnlyPaths = [
+ "/nix/store"
+ "-/etc/resolv.conf"
+ "-/etc/nsswitch.conf"
+ "-/etc/hosts"
+ "-/etc/localtime"
+ "-/etc/dex"
+ ];
+ BindPaths = [ cfg.stateDir ] ++ lib.optional (settings.storage.type == "postgres") "/var/run/postgresql";
+ CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
+ ## ProtectClock= adds DeviceAllow=char-rtc r
+ #DeviceAllow = "";
+ #DynamicUser = true;
+ #LockPersonality = true;
+ #MemoryDenyWriteExecute = true;
+ #NoNewPrivileges = true;
+ #PrivateDevices = true;
+ #PrivateMounts = true;
+ ## Port needs to be exposed to the host network
+ ##PrivateNetwork = true;
+ #PrivateTmp = true;
+ #PrivateUsers = true;
+ #ProcSubset = "pid";
+ #ProtectClock = true;
+ #ProtectHome = true;
+ #ProtectHostname = true;
+ ## Would re-mount paths ignored by temporary root
+ ##ProtectSystem = "strict";
+ #ProtectControlGroups = true;
+ #ProtectKernelLogs = true;
+ #ProtectKernelModules = true;
+ #ProtectKernelTunables = true;
+ #ProtectProc = "invisible";
+ #RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+ #RestrictNamespaces = true;
+ #RestrictRealtime = true;
+ #RestrictSUIDSGID = true;
+ #SystemCallArchitectures = "native";
+ #SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
+ #TemporaryFileSystem = "/:ro";
+ # Does not work well with the temporary root
+ #UMask = "0066";
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/modules/nixos/services/dex/default.orig.nix b/modules/nixos/services/dex/default.orig.nix
new file mode 100644
index 0000000..3ec64ef
--- /dev/null
+++ b/modules/nixos/services/dex/default.orig.nix
@@ -0,0 +1,121 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.dex;
+ fixClient = client: if client ? secretFile then ((builtins.removeAttrs client [ "secretFile" ]) // { secret = client.secretFile; }) else client;
+ filteredSettings = mapAttrs (n: v: if n == "staticClients" then (builtins.map fixClient v) else v) cfg.settings;
+ secretFiles = flatten (builtins.map (c: if c ? secretFile then [ c.secretFile ] else [ ]) (cfg.settings.staticClients or [ ]));
+
+ settingsFormat = pkgs.formats.yaml { };
+ configFile = settingsFormat.generate "config.yaml" filteredSettings;
+
+ startPreScript = pkgs.writeShellScript "dex-start-pre" (''
+ '' + (concatStringsSep "\n" (builtins.map
+ (file: ''
+ ${pkgs.replace-secret}/bin/replace-secret '${file}' '${file}' /run/dex/config.yaml
+ '')
+ secretFiles)));
+in
+{
+ options.services.dex = {
+ enable = mkEnableOption "the OpenID Connect and OAuth2 identity provider";
+
+ settings = mkOption {
+ type = settingsFormat.type;
+ default = { };
+ example = literalExpression ''
+ {
+ # External url
+ issuer = "http://127.0.0.1:5556/dex";
+ storage = {
+ type = "postgres";
+ config.host = "/var/run/postgres";
+ };
+ web = {
+ http = "127.0.0.1:5556";
+ };
+ enablePasswordDB = true;
+ staticClients = [
+ {
+ id = "oidcclient";
+ name = "Client";
+ redirectURIs = [ "https://example.com/callback" ];
+ secretFile = "/etc/dex/oidcclient"; # The content of `secretFile` will be written into to the config as `secret`.
+ }
+ ];
+ }
+ '';
+ description = ''
+ The available options can be found in
+ the example configuration.
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+ systemd.services.dex = {
+ description = "dex identity provider";
+ wantedBy = [ "multi-user.target" ];
+ after = [ "networking.target" ] ++ (optional (cfg.settings.storage.type == "postgres") "postgresql.service");
+
+ serviceConfig = {
+ ExecStart = "${pkgs.dex-oidc}/bin/dex serve /run/dex/config.yaml";
+ ExecStartPre = [
+ "${pkgs.coreutils}/bin/install -m 600 ${configFile} /run/dex/config.yaml"
+ "+${startPreScript}"
+ ];
+ RuntimeDirectory = "dex";
+
+ AmbientCapabilities = "CAP_NET_BIND_SERVICE";
+ BindReadOnlyPaths = [
+ "/nix/store"
+ "-/etc/resolv.conf"
+ "-/etc/nsswitch.conf"
+ "-/etc/hosts"
+ "-/etc/localtime"
+ "-/etc/dex"
+ ];
+ BindPaths = optional (cfg.settings.storage.type == "postgres") "/var/run/postgresql";
+ CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
+ # ProtectClock= adds DeviceAllow=char-rtc r
+ DeviceAllow = "";
+ DynamicUser = true;
+ LockPersonality = true;
+ MemoryDenyWriteExecute = true;
+ NoNewPrivileges = true;
+ PrivateDevices = true;
+ PrivateMounts = true;
+ # Port needs to be exposed to the host network
+ #PrivateNetwork = true;
+ PrivateTmp = true;
+ PrivateUsers = true;
+ ProcSubset = "pid";
+ ProtectClock = true;
+ ProtectHome = true;
+ ProtectHostname = true;
+ # Would re-mount paths ignored by temporary root
+ #ProtectSystem = "strict";
+ ProtectControlGroups = true;
+ ProtectKernelLogs = true;
+ ProtectKernelModules = true;
+ ProtectKernelTunables = true;
+ ProtectProc = "invisible";
+ RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+ RestrictNamespaces = true;
+ RestrictRealtime = true;
+ RestrictSUIDSGID = true;
+ SystemCallArchitectures = "native";
+ SystemCallFilter = [ "@system-service" "~@privileged @resources @setuid @keyring" ];
+ TemporaryFileSystem = "/:ro";
+ # Does not work well with the temporary root
+ #UMask = "0066";
+ };
+ };
+ };
+
+ # uses attributes of the linked package
+ meta.buildDocsInSandbox = false;
+}
+
diff --git a/modules/nixos/services/homer/default.nix b/modules/nixos/services/homer/default.nix
new file mode 100644
index 0000000..44ac2e5
--- /dev/null
+++ b/modules/nixos/services/homer/default.nix
@@ -0,0 +1,79 @@
+{ lib, config, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.services.homer;
+
+ yaml-format = pkgs.formats.yaml { };
+ settings-yaml = yaml-format.generate "config.yml" cfg.settings;
+
+ settings-path =
+ if cfg.settings-path != null then
+ cfg.settings-path
+ else
+ builtins.toString settings-yaml;
+in
+{
+ options.plusultra.services.homer = {
+ enable = mkEnableOption "Homer";
+
+ package = mkOpt types.package pkgs.plusultra.homer "The package of Homer assets to use.";
+
+ settings = mkOpt yaml-format.type { } "Configuration for Homer's config.yml file.";
+ settings-path = mkOpt (types.nullOr types.path) null "A replacement for the generated config.yml file.";
+
+ host = mkOpt (types.nullOr types.str) null "The host to serve Homer on.";
+
+ nginx = {
+ forceSSL = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Whether or not to force the use of SSL.";
+ };
+ };
+
+ acme = {
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ description =
+ "Whether or not to automatically fetch and configure SSL certs.";
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ assertions = [
+ {
+ assertion = cfg.host != null;
+ message = "plusultra.services.homer.host must be set.";
+ }
+ {
+ assertion = cfg.settings-path != null -> cfg.settings == { };
+ message = "plusultra.services.homer.settings and plusultra.services.homer.settings-path are mutually exclusive.";
+ }
+ {
+ assertion = cfg.nginx.forceSSL -> cfg.acme.enable;
+ message = "plusultra.services.homer.nginx.forceSSL requires setting plusultra.services.homer.acme.enable to true.";
+ }
+ ];
+
+ services.nginx = {
+ enable = true;
+
+ virtualHosts."${cfg.host}" = {
+ enableACME = cfg.acme.enable;
+ forceSSL = cfg.nginx.forceSSL;
+
+ locations."/" = {
+ root = "${cfg.package}/share/homer";
+ };
+
+ locations."= /assets/config.yml" = {
+ alias = settings-path;
+ };
+ };
+ };
+ };
+}
diff --git a/modules/nixos/services/infrared/default.nix b/modules/nixos/services/infrared/default.nix
new file mode 100644
index 0000000..220d139
--- /dev/null
+++ b/modules/nixos/services/infrared/default.nix
@@ -0,0 +1,158 @@
+{ config, options, lib, pkgs, ... }:
+
+let
+ inherit (builtins) toString;
+ inherit (lib) types;
+
+ cfg = config.plusultra.services.infrared;
+
+ format = pkgs.formats.json { };
+
+ serversType = (types.submodule ({ config, ... }: {
+ options = {
+ domain = lib.mkOption {
+ type = types.str;
+ default = "";
+ description = ''
+ The domain to proxy. Should be fully qualified domain name.
+ Note: Every string is accepted. So localhost is also valid.
+ '';
+ example = "minecraft.example.com";
+ };
+
+ host = lib.mkOption {
+ type = types.str;
+ default = "";
+ description = "The host where the Minecraft server is running. Defaults to local host.";
+ };
+
+ port = lib.mkOption {
+ type = types.port;
+ default = 25566;
+ description = "The port where the Minecraft server is running.";
+ };
+
+ settings = lib.mkOption {
+ default = { };
+ description = ''
+ Infrared configuration (config.json). Refer to
+
+ for details.
+ '';
+
+ type = types.submodule {
+ freeformType = format.type;
+
+ options = {
+ domainName = lib.mkOption {
+ type = types.str;
+ default = config.domain;
+ defaultText = lib.literalExpression ''
+ ""
+ '';
+ description = "The domain to proxy.";
+ };
+
+ proxyTo = lib.mkOption {
+ type = types.str;
+ default = "${config.host}:${toString config.port}";
+ defaultText = ":25565";
+ description = "The address that the proxy should send incoming connections to.";
+ };
+ };
+ };
+ };
+ };
+ }));
+in
+{
+ options.plusultra.services.infrared = {
+ enable = lib.mkEnableOption "Infrared";
+
+ stateDir = lib.mkOption {
+ type = types.path;
+ default = "/var/lib/infrared";
+ description = "The state directory where configurations are stored.";
+ };
+
+ user = lib.mkOption {
+ type = types.str;
+ default = "infrared";
+ description = "User under which Infrared is ran.";
+ };
+
+ group = lib.mkOption {
+ type = types.str;
+ default = "infrared";
+ description = "Group under which Infrared is ran.";
+ };
+
+ openFirewall = lib.mkOption {
+ type = types.bool;
+ default = false;
+ description = "Whether to open the firewall for ports specified by each server's listenTo address.";
+ };
+
+ servers = lib.mkOption {
+ type = types.listOf serversType;
+ default = [ ];
+ description = "The servers to proxy.";
+ example = lib.literalExpression ''
+ [
+ {
+ domain = "minecraft.example.com";
+ port = 25567;
+ }
+ ]
+ '';
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ assertions = [
+ ];
+
+ networking.firewall = lib.mkIf cfg.openFirewall {
+ allowedUDPPorts = builtins.map (server: server.port) cfg.servers;
+ allowedTCPPorts = builtins.map (server: server.port) cfg.servers;
+ };
+
+ systemd.tmpfiles.rules =
+ [ "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" ] ++
+ builtins.map
+ (server:
+ let
+ config = format.generate "${server.domain}.json" server.settings;
+ in
+ "L+ '${cfg.stateDir}/${server.domain}.json' - - - - ${config}"
+ )
+ cfg.servers;
+
+ systemd.services.infrared = {
+ after = [ "network.target" ];
+ wantedBy = [ "multi-user.target" ];
+
+ serviceConfig = {
+ Type = "simple";
+ User = cfg.user;
+ Group = cfg.group;
+ WorkingDirectory = cfg.stateDir;
+ ExecStart = "${pkgs.plusultra.infrared}/bin/infrared -config-path ${cfg.stateDir}";
+ };
+ };
+
+ users = {
+ users = lib.optionalAttrs (cfg.user == "infrared") {
+ infrared = {
+ group = cfg.group;
+ home = "/var/lib/infrared";
+ isSystemUser = true;
+ };
+ };
+
+ groups = lib.optionalAttrs (cfg.group == "infrared") {
+ infrared = { };
+ };
+ };
+ };
+}
diff --git a/modules/nixos/services/minecraft/default.nix b/modules/nixos/services/minecraft/default.nix
new file mode 100644
index 0000000..9fbe2a9
--- /dev/null
+++ b/modules/nixos/services/minecraft/default.nix
@@ -0,0 +1,195 @@
+{ config, options, lib, pkgs, ... }:
+
+let
+ inherit (lib) types;
+
+ cfg = config.plusultra.services.minecraft;
+in
+{
+ options.plusultra.services.minecraft = {
+ enable = lib.mkEnableOption "Minecraft server";
+
+ eula = lib.mkOption {
+ type = types.bool;
+ default = false;
+ description = lib.mdDoc ''
+ Whether you agree to
+ [Mojang's EULA](https://account.mojang.com/documents/minecraft_eula).
+ This option must be set to `true` to run Minecraft server.
+ '';
+ };
+
+ infrared = {
+ enable = lib.mkEnableOption "Infrared";
+ };
+
+ servers = lib.mkOption {
+ default = { };
+ description = "The Minecraft servers to run.";
+ example = lib.literalExpression ''
+ {
+ # A default vanilla server.
+ vanilla-1 = {};
+
+ # A vanilla server with a custom port.
+ vanilla-2 = {
+ port = 4000;
+ };
+
+ # A vanilla server proxied by Infrared (when enabled).
+ vanilla-3 = {
+ domain = "minecraft.example.com";
+ };
+
+ # A Forge server.
+ forge-1 = {
+ type = "forge";
+ };
+
+ # Use a custom Minecraft server version.
+ custom-vanilla = {
+ package = pkgs.minecraft-server_1_12_2;
+ };
+
+ # Use a custom Forge server version.
+ custom-forge = {
+ type = "forge";
+ package = pkgs.minecraft-forge_1_19_2-43_1_25;
+ };
+ }
+ '';
+
+ type = types.attrsOf (types.submodule ({ config, name, ... }: {
+ options = {
+ type = lib.mkOption {
+ type = types.enum [ "vanilla" "forge" ];
+ default = "vanilla";
+ description = "The kind of Minecraft server to create.";
+ };
+
+ package = lib.mkOption {
+ type = types.package;
+ default =
+ if config.type == "vanilla" then
+ pkgs.minecraft-server
+ else
+ pkgs.plusultra.minecraft-forge;
+ defaultText = lib.literalExpression ''
+ pkgs.minecraft-server
+ '';
+ };
+
+ dataDir = lib.mkOption {
+ type = types.path;
+ default = "/var/lib/minecraft/${name}";
+ defaultText = "/var/lib/minecraft/";
+ description = "The datrectory where data for the server is stored.";
+ };
+
+ port = lib.mkOption {
+ type = types.port;
+ default = 25565;
+ description = "The port for the server to listen on.";
+ };
+
+ domain = lib.mkOption {
+ type = types.str;
+ default = "";
+ description = "The domain to pass to Infrared (if enabled).";
+ };
+
+ jvmOpts = lib.mkOption {
+ type = types.separatedString " ";
+ default = "-Xmx2048M -Xms2048M";
+ # Example options from https://minecraft.gamepedia.com/Tutorials/Server_startup_script
+ example = "-Xms4092M -Xmx4092M -XX:+UseG1GC -XX:+CMSIncrementalPacing "
+ + "-XX:+CMSClassUnloadingEnabled -XX:ParallelGCThreads=2 "
+ + "-XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=10";
+ description = lib.mdDoc "JVM options for the Minecraft server.";
+ };
+
+ serverProperties = lib.mkOption {
+ type = types.attrsOf (types.oneOf [ types.bool types.int types.str ]);
+ default = { };
+ example = lib.literalExpression ''
+ {
+ server-port = 43000;
+ difficulty = 3;
+ gamemode = 1;
+ max-players = 5;
+ motd = "NixOS Minecraft server!";
+ white-list = true;
+ enable-rcon = true;
+ "rcon.password" = "hunter2";
+ }
+ '';
+ description = lib.mdDoc ''
+ Minecraft server properties for the server.properties file. Only has
+ an effect when {option}`services.minecraft-server.declarative`
+ is set to `true`. See
+
+ for documentation on these values.
+ '';
+ };
+
+ whitelist = lib.mkOption {
+ type =
+ let
+ minecraftUUID = lib.types.strMatching
+ "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" // {
+ description = "Minecraft UUID";
+ };
+ in
+ lib.types.attrsOf minecraftUUID;
+ default = { };
+ description = lib.mdDoc ''
+ Whitelisted players, only has an effect when
+ {option}`services.minecraft-server.declarative` is
+ `true` and the whitelist is enabled
+ via {option}`services.minecraft-server.serverProperties` by
+ setting `white-list` to `true`.
+ This is a mapping from Minecraft usernames to UUIDs.
+ You can use to get a
+ Minecraft UUID for a username.
+ '';
+ example = lib.literalExpression ''
+ {
+ username1 = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
+ username2 = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy";
+ };
+ '';
+ };
+
+ openFirewall = lib.mkOption {
+ type = types.bool;
+ default = false;
+ description = lib.mdDoc ''
+ Whether to open ports in the firewall for the server.
+ '';
+ };
+
+ declarative = lib.mkOption {
+ type = types.bool;
+ default = false;
+ description = lib.mdDoc ''
+ Whether to use a declarative Minecraft server configuration.
+ Only if set to `true`, the options
+ {option}`plusultra.services.minecraft.servers..whitelist` and
+ {option}`plusultra.services.minecraft.servers..serverProperties` will be
+ applied.
+ '';
+ };
+
+ extraInfraredOptions = lib.mkOption {
+ type = types.attrs;
+ default = { };
+
+ description = lib.mdDoc ''
+ Extra options passed to Infrared (if enabled) when configuring this server.
+ '';
+ };
+ };
+ }));
+ };
+ };
+}
diff --git a/modules/nixos/services/openssh/default.nix b/modules/nixos/services/openssh/default.nix
new file mode 100644
index 0000000..ea9b1d3
--- /dev/null
+++ b/modules/nixos/services/openssh/default.nix
@@ -0,0 +1,109 @@
+{ options
+, config
+, pkgs
+, lib
+, host ? ""
+, format ? ""
+, inputs ? { }
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.services.openssh;
+
+ user = config.users.users.${config.plusultra.user.name};
+ user-id = builtins.toString user.uid;
+
+ # TODO: This is a hold-over from an earlier Snowfall Lib version which used
+ # the specialArg `name` to provide the host name.
+ name = host;
+
+ default-key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCwaaCUq3Ooq1BaHbg5IwVxWj/xmNJY2dDthHKPZefrHXv/ksM/IREgm38J0CdoMpVS0Zp1C/vFrwGfaYZ2lCF5hBVdV3gf+mvj8Yb8Xpm6aM4L5ig+oBMp/3cz1+g/I4aLMJfCKCtdD6Q2o4vtkTpid6X+kL3UGZbX0HFn3pxoDinzOXQnVGSGw+pQhLASvQeVXWTJjVfIWhj9L2NRJau42cBRRlAH9kE3HUbcgLgyPUZ28aGXLLmiQ6CUjiIlce5ee16WNLHQHOzVfPJfF1e1F0HwGMMBe39ey3IEQz6ab1YqlIzjRx9fQ9hQK6Du+Duupby8JmBlbUAxhh8KJFCJB2cXW/K5Et4R8GHMS6MyIoKQwFUXGyrszVfiuNTGZIkPAYx9zlCq9M/J+x1xUZLHymL85WLPyxhlhN4ysM9ILYiyiJ3gYrPIn5FIZrW7MCQX4h8k0bEjWUwH5kF3dZpEvIT2ssyIu12fGzXkYaNQcJEb5D9gT1mNyi2dxQ62NPZ5orfYyIZ7fn22d1P/jegG+7LQeXPiy5NLE6b7MP5Rq2dL8Y9Oi8pOBtoY9BpLh7saSBbNFXTBtH/8OfAQacxDsZD/zTFtCzZjtTK6yiAaXCZTvMIOuoYGZvEk6zWXrjVsU8FlqF+4JOTfePqr/SSUXNJyKnrvQJ1BfHQiYsrckw==";
+
+ other-hosts =
+ lib.filterAttrs
+ (key: host:
+ key != name && (host.config.plusultra.user.name or null) != null)
+ ((inputs.self.nixosConfigurations or { }) // (inputs.self.darwinConfigurations or { }));
+
+ other-hosts-config =
+ lib.concatMapStringsSep
+ "\n"
+ (
+ name:
+ let
+ remote = other-hosts.${name};
+ remote-user-name = remote.config.plusultra.user.name;
+ remote-user-id = builtins.toString remote.config.users.users.${remote-user-name}.uid;
+
+ forward-gpg =
+ optionalString (config.programs.gnupg.agent.enable && remote.config.programs.gnupg.agent.enable)
+ ''
+ RemoteForward /run/user/${remote-user-id}/gnupg/S.gpg-agent /run/user/${user-id}/gnupg/S.gpg-agent.extra
+ RemoteForward /run/user/${remote-user-id}/gnupg/S.gpg-agent.ssh /run/user/${user-id}/gnupg/S.gpg-agent.ssh
+ '';
+ in
+ ''
+ Host ${name}
+ User ${remote-user-name}
+ ForwardAgent yes
+ Port ${builtins.toString cfg.port}
+ ${forward-gpg}
+ ''
+ )
+ (builtins.attrNames other-hosts);
+in
+{
+ options.plusultra.services.openssh = with types; {
+ enable = mkBoolOpt false "Whether or not to configure OpenSSH support.";
+ authorizedKeys =
+ mkOpt (listOf str) [ default-key ] "The public keys to apply.";
+ port = mkOpt port 2222 "The port to listen on (in addition to 22).";
+ manage-other-hosts = mkOpt bool true "Whether or not to add other host configurations to SSH config.";
+ };
+
+ config = mkIf cfg.enable {
+ services.openssh = {
+ enable = true;
+
+ settings = {
+ PermitRootLogin =
+ if format == "install-iso"
+ then "yes"
+ else "no";
+ PasswordAuthentication = false;
+ };
+
+ extraConfig = ''
+ StreamLocalBindUnlink yes
+ '';
+
+ ports = [
+ 22
+ cfg.port
+ ];
+ };
+
+ programs.ssh.extraConfig = ''
+ Host *
+ HostKeyAlgorithms +ssh-rsa
+
+ ${optionalString cfg.manage-other-hosts other-hosts-config}
+ '';
+
+ plusultra.user.extraOptions.openssh.authorizedKeys.keys =
+ cfg.authorizedKeys;
+
+ plusultra.home.extraOptions = {
+ programs.zsh.shellAliases =
+ foldl
+ (aliases: system:
+ aliases
+ // {
+ "ssh-${system}" = "ssh ${system} -t tmux a";
+ })
+ { }
+ (builtins.attrNames other-hosts);
+ };
+ };
+}
diff --git a/modules/nixos/services/printing/default.nix b/modules/nixos/services/printing/default.nix
new file mode 100644
index 0000000..d6884e8
--- /dev/null
+++ b/modules/nixos/services/printing/default.nix
@@ -0,0 +1,13 @@
+{ options, config, pkgs, lib, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.services.printing;
+in
+{
+ options.plusultra.services.printing = with types; {
+ enable = mkBoolOpt false "Whether or not to configure printing support.";
+ };
+
+ config = mkIf cfg.enable { services.printing.enable = true; };
+}
diff --git a/modules/nixos/services/samba/default.nix b/modules/nixos/services/samba/default.nix
new file mode 100644
index 0000000..ba5fdec
--- /dev/null
+++ b/modules/nixos/services/samba/default.nix
@@ -0,0 +1,76 @@
+{ lib, config, ... }:
+
+let
+ cfg = config.plusultra.services.samba;
+
+ inherit (lib)
+ types
+ mkEnableOption
+ mkIf
+ mapAttrs
+ optionalAttrs;
+
+ inherit (lib.plusultra)
+ mkOpt
+ mkBoolOpt;
+
+ bool-to-yes-no = value: if value then "yes" else "no";
+
+ shares-submodule = with types; submodule ({ name, ... }: {
+ options = {
+ path = mkOpt str null "The path to serve.";
+ public = mkBoolOpt false "Whether the share is public.";
+ browseable = mkBoolOpt true "Whether the share is browseable.";
+ comment = mkOpt str name "An optional comment.";
+ read-only = mkBoolOpt false "Whether the share should be read only.";
+ only-owner-editable = mkBoolOpt false "Whether the share is only writable by the system owner (plusultra.user.name).";
+
+ extra-config = mkOpt attrs { } "Extra configuration options for the share.";
+ };
+ });
+in
+{
+ options.plusultra.services.samba = with types; {
+ enable = mkEnableOption "Samba";
+ workgroup = mkOpt str "WORKGROUP" "The workgroup to use.";
+ browseable = mkBoolOpt true "Whether the shares are browseable.";
+
+ shares = mkOpt (attrsOf shares-submodule) { } "The shares to serve.";
+ };
+
+ config = mkIf cfg.enable {
+ networking.firewall = {
+ allowedTCPPorts = [ 5357 ];
+ allowedUDPPorts = [ 3702 ];
+ };
+
+ services.samba-wsdd = {
+ enable = true;
+ discovery = true;
+ workgroup = "WORKGROUP";
+ };
+
+ services.samba = {
+ enable = true;
+ openFirewall = true;
+
+ extraConfig = ''
+ browseable = ${bool-to-yes-no cfg.browseable}
+ '';
+
+ shares = mapAttrs
+ (name: value: {
+ inherit (value) path comment;
+
+ public = bool-to-yes-no value.public;
+ browseable = bool-to-yes-no value.browseable;
+ "read only" = bool-to-yes-no value.read-only;
+ } // (optionalAttrs value.only-owner-editable {
+ "write list" = config.plusultra.user.name;
+ "read list" = "guest, nobody";
+ "create mask" = "0755";
+ }) // value.extra-config)
+ cfg.shares;
+ };
+ };
+}
diff --git a/modules/nixos/services/tailscale/default.nix b/modules/nixos/services/tailscale/default.nix
new file mode 100644
index 0000000..02ae1a2
--- /dev/null
+++ b/modules/nixos/services/tailscale/default.nix
@@ -0,0 +1,69 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.services.tailscale;
+in
+{
+ options.plusultra.services.tailscale = with types; {
+ enable = mkBoolOpt false "Whether or not to configure Tailscale";
+ autoconnect = {
+ enable = mkBoolOpt false "Whether or not to enable automatic connection to Tailscale";
+ key = mkOpt str "" "The authentication key to use";
+ };
+ };
+
+ config = mkIf cfg.enable {
+ assertions = [
+ {
+ assertion = cfg.autoconnect.enable -> cfg.autoconnect.key != "";
+ message = "plusultra.services.tailscale.autoconnect.key must be set";
+ }
+ ];
+
+ environment.systemPackages = with pkgs; [ tailscale ];
+
+ services.tailscale = enabled;
+
+ networking = {
+ firewall = {
+ trustedInterfaces = [ config.services.tailscale.interfaceName ];
+
+ allowedUDPPorts = [ config.services.tailscale.port ];
+
+ # Strict reverse path filtering breaks Tailscale exit node use and some subnet routing setups.
+ checkReversePath = "loose";
+ };
+
+ networkmanager.unmanaged = [ "tailscale0" ];
+ };
+
+ systemd.services.tailscale-autoconnect = mkIf cfg.autoconnect.enable {
+ description = "Automatic connection to Tailscale";
+
+ # Make sure tailscale is running before trying to connect to tailscale
+ after = [ "network-pre.target" "tailscale.service" ];
+ wants = [ "network-pre.target" "tailscale.service" ];
+ wantedBy = [ "multi-user.target" ];
+
+ # Set this service as a oneshot job
+ serviceConfig.Type = "oneshot";
+
+ # Have the job run this shell script
+ script = with pkgs; ''
+ # Wait for tailscaled to settle
+ sleep 2
+
+ # Check if we are already authenticated to tailscale
+ status="$(${tailscale}/bin/tailscale status -json | ${jq}/bin/jq -r .BackendState)"
+ if [ $status = "Running" ]; then # if so, then do nothing
+ exit 0
+ fi
+
+ # Otherwise authenticate with tailscale
+ ${tailscale}/bin/tailscale up -authkey "${cfg.autoconnect.key}"
+ '';
+
+ };
+ };
+}
diff --git a/modules/nixos/services/vault-agent/default.nix b/modules/nixos/services/vault-agent/default.nix
new file mode 100644
index 0000000..1d68787
--- /dev/null
+++ b/modules/nixos/services/vault-agent/default.nix
@@ -0,0 +1,174 @@
+{ lib, config, pkgs, inputs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.services.vault-agent;
+
+ # nixos-vault-service places generated files here:
+ # https://github.com/DeterminateSystems/nixos-vault-service/blob/45e65627dff5dc4bb40d0f2595916f37e78959c1/module/helpers.nix#L4
+ secret-files-root = "/tmp/detsys-vault";
+ environment-files-root = "/run/keys/environment";
+
+ create-environment-files-submodule = service-name: types.submodule ({ name, ... }: {
+ options = {
+ text = mkOpt (types.nullOr types.str) null "An inline template for Vault to template.";
+ source = mkOpt (types.nullOr types.path) null "The file with environment variables for Vault to template.";
+ path = mkOption {
+ readOnly = true;
+ type = types.str;
+ description = "The path to the environment file.";
+ default = "${environment-files-root}/${service-name}/${name}.EnvFile";
+ defaultText = "${environment-files-root}//.EnvFile";
+ };
+ };
+ });
+
+ secret-files-submodule = types.submodule ({ name, ... }: {
+ options = {
+ text = mkOpt (types.nullOr types.str) null "An inline template for Vault to template.";
+ source = mkOpt (types.nullOr types.path) null "The file for Vault to template.";
+ permissions = mkOpt types.str "0400" "The octal mode of this file.";
+ change-action = mkOpt (types.nullOr (types.enum [ "restart" "stop" "none" ])) null "The action to take when secrets change.";
+ path = mkOption {
+ readOnly = true;
+ type = types.str;
+ description = "The path to the secret file.";
+ default = "${secret-files-root}/${name}";
+ };
+ };
+ });
+
+
+ services-submodule =
+ types.submodule
+ ({ name, config, ... }: {
+ options = {
+ enable = mkBoolOpt true "Whether to enable Vault Agent for this service.";
+ settings = mkOpt types.attrs { } "Vault Agent configuration.";
+ secrets = {
+ environment = {
+ force = mkOpt types.bool false "Whether or not to force the use of Vault Agent's environment files.";
+ change-action = mkOpt (types.enum [ "restart" "stop" "none" ]) "restart" "The action to take when secrets change.";
+ templates = mkOpt (types.attrsOf (create-environment-files-submodule name)) { } "Environment variable files for Vault to template.";
+ template = mkOpt (types.nullOr (types.either types.path types.str)) null "An environment variable template.";
+ paths = mkOption {
+ readOnly = true;
+ type = types.listOf types.str;
+ description = "Paths to all of the environment files";
+ default =
+ if config.secrets.environment.template != null then
+ [ "${environment-files-root}/${name}/EnvFile" ]
+ else
+ (
+ mapAttrsToList
+ (template-name: value: value.path)
+ config.secrets.environment.templates
+ );
+ };
+ };
+
+ file = {
+ change-action = mkOpt (types.enum [ "restart" "stop" "none" ]) "restart" "The action to take when secrets change.";
+ files = mkOption {
+ description = "Secret files to template.";
+ default = { };
+ type = types.attrsOf secret-files-submodule;
+ };
+ };
+ };
+ };
+ });
+in
+{
+ # imports = [
+ # inputs.vault-service.nixosModules.nixos-vault-service
+ # ];
+
+ options.plusultra.services.vault-agent = {
+ enable = mkEnableOption "Vault Agent";
+
+ settings = mkOpt types.attrs { } "Default Vault Agent configuration.";
+
+ services = mkOption {
+ description = "Services to install Vault Agent into.";
+ default = { };
+ type = types.attrsOf services-submodule;
+ };
+ };
+
+ config = mkIf cfg.enable {
+ assertions = flatten (mapAttrsToList
+ (service-name: service:
+ (mapAttrsToList
+ (template-name: template:
+ {
+ assertion = (template.source != null && template.text == null) || (template.source == null && template.text != null);
+ message = "plusultra.services.vault-agent.services.${service-name}.secrets.environment.templates.${template-name} must set either `source` or `text`.";
+ }
+ )
+ service.secrets.environment.templates)
+ ++
+ (mapAttrsToList
+ (file-name: file:
+ {
+ assertion = (file.source != null && file.text == null) || (file.source == null && file.text != null);
+ message = "plusultra.services.vault-agent.services.${service-name}.secrets.file.files.${file-name} must set either `source` or `text`.";
+ }
+ )
+ service.secrets.file.files)
+ )
+ cfg.services);
+
+ systemd.services = mapAttrs
+ (service-name: value: mkIf value.secrets.environment.force {
+ serviceConfig.EnvironmentFile = mkForce value.secrets.environment.paths;
+ })
+ cfg.services;
+
+ detsys.vaultAgent = {
+ defaultAgentConfig = cfg.settings;
+
+ systemd.services = mapAttrs
+ (service-name: value: {
+ inherit (value) enable;
+
+ agentConfig = value.settings;
+
+ environment = {
+ changeAction = value.secrets.environment.change-action;
+
+ templateFiles = mapAttrs
+ (template-name: value: {
+ file =
+ if value.source != null then
+ value.source
+ else
+ pkgs.writeText "${service-name}-${template-name}-env-template" value.text;
+ })
+ value.secrets.environment.templates;
+
+ template =
+ if (builtins.isPath value.secrets.environment.template) || (builtins.isNull value.secrets.environment.template) then
+ value.secrets.environment.template
+ else
+ pkgs.writeText "${service-name}-env-template" value.secrets.environment.template;
+ };
+
+ secretFiles = {
+ defaultChangeAction = value.secrets.file.change-action;
+
+ files = mapAttrs
+ (file-name: value: {
+ changeAction = value.change-action;
+ template = value.text;
+ templateFile = value.source;
+ perms = value.permissions;
+ })
+ value.secrets.file.files;
+ };
+ })
+ cfg.services;
+ };
+ };
+}
diff --git a/modules/nixos/services/vault/default.nix b/modules/nixos/services/vault/default.nix
new file mode 100644
index 0000000..587f0f6
--- /dev/null
+++ b/modules/nixos/services/vault/default.nix
@@ -0,0 +1,202 @@
+{ lib, config, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.services.vault;
+
+ package = if cfg.ui then pkgs.vault-bin else pkgs.vault;
+
+ has-policies = (builtins.length (builtins.attrNames cfg.policies)) != 0;
+
+ format-policy = name: file: pkgs.runCommandNoCC
+ "formatted-vault-policy"
+ {
+ inherit file;
+ buildInputs = [ package ];
+ }
+ ''
+ name="$(basename "$file")"
+
+ cp "$file" "./$name"
+
+ # Ensure that vault can overwrite the file.
+ chmod +w "./$name"
+
+ # Create this variable here to avoid swallowing vault's exit code.
+ vault_output=
+
+ set +e
+ vault_output=$(vault policy fmt "./$name" 2>&1)
+ vault_status=$?
+ set -e
+
+ if [ "$vault_status" != 0 ]; then
+ echo 'Error formatting policy "${name}"'
+ echo "This is normally caused by a syntax error in the policy file."
+ echo "$file"
+ echo ""
+ echo "Vault Output:"
+ echo "$vault_output"
+ exit 1
+ fi
+
+ mv "./$name" $out
+ '';
+
+ policies = mapAttrs
+ (name: value:
+ if builtins.isPath value then
+ format-policy name value
+ else
+ format-policy name (pkgs.writeText "${name}.hcl" value)
+ )
+ cfg.policies;
+in
+{
+ options.plusultra.services.vault = {
+ enable = mkEnableOption "Vault";
+
+ ui = mkBoolOpt true "Whether the UI should be enabled.";
+
+ storage = {
+ backend = mkOpt types.str "file" "The storage backend for Vault.";
+ };
+
+ settings = mkOpt types.str "" "Configuration for Vault's config file.";
+
+ mutable-policies = mkBoolOpt false "Whether policies not specified in Nix should be removed.";
+
+ policies = mkOpt (types.attrsOf (types.either types.str types.path)) { } "Policies to install when Vault runs.";
+
+ policy-agent = {
+ user = mkOpt types.str "vault" "The user to run the Vault Agent as.";
+ group = mkOpt types.str "vault" "The group to run the Vault Agent as.";
+
+ auth = {
+ roleIdFilePath = mkOpt types.str "/var/lib/vault/role-id" "The file to read the role-id from.";
+ secretIdFilePath = mkOpt types.str "/var/lib/vault/secret-id" "The file to read the secret-id from.";
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ services.vault = {
+ enable = true;
+ inherit package;
+
+ extraConfig = ''
+ ui = ${if cfg.ui then "true" else "false"}
+
+ ${cfg.settings}
+ '';
+
+ };
+
+ systemd.services.vault = { };
+
+ systemd.services.vault-policies = mkIf (has-policies || !cfg.mutable-policies) {
+ wantedBy = [ "vault.service" ];
+ after = [ "vault.service" ];
+
+ serviceConfig = {
+ Type = "oneshot";
+ User = cfg.policy-agent.user;
+ Group = cfg.policy-agent.group;
+ Restart = "on-failure";
+ RestartSec = 30;
+ RemainAfterExit = "yes";
+ };
+
+ restartTriggers = (mapAttrsToList (name: value: "${name}=${value}") policies);
+
+ path = [
+ package
+ pkgs.curl
+ pkgs.jq
+ ];
+
+ environment = {
+ VAULT_ADDR = "http://${config.services.vault.address}";
+ };
+
+ script =
+ let
+ write-policies-commands = mapAttrsToList
+ (name: policy:
+ ''
+ echo Writing policy '${name}': '${policy}'
+ vault policy write '${name}' '${policy}'
+ ''
+ )
+ policies;
+ write-policies = concatStringsSep "\n" write-policies-commands;
+
+ known-policies = mapAttrsToList (name: value: name) policies;
+
+ remove-unknown-policies = ''
+ current_policies=$(vault policy list -format=json | jq -r '.[]')
+ known_policies=(${concatStringsSep " " (builtins.map (policy: "\"${policy}\"") known-policies)})
+
+ while read current_policy; do
+ is_known=false
+
+ for known_policy in "''${known_policies[@]}"; do
+ if [ "$known_policy" = "$current_policy" ]; then
+ is_known=true
+ break
+ fi
+ done
+
+ if [ "$is_known" = "false" ] && [ "$current_policy" != "default" ] && [ "$current_policy" != "root" ]; then
+ echo "Removing policy: $current_policy"
+ vault policy delete "$current_policy"
+ else
+ echo "Keeping policy: $current_policy"
+ fi
+ done <<< "$current_policies"
+ '';
+ in
+ ''
+ if ! [ -f '${cfg.policy-agent.auth.roleIdFilePath}' ]; then
+ echo 'role-id file not found: ${cfg.policy-agent.auth.roleIdFilePath}'
+ exit 1
+ fi
+
+ if ! [ -f '${cfg.policy-agent.auth.secretIdFilePath}' ]; then
+ echo 'secret-id file not found: ${cfg.policy-agent.auth.secretIdFilePath}'
+ exit 1
+ fi
+
+ role_id="$(cat '${cfg.policy-agent.auth.roleIdFilePath}')"
+ secret_id="$(cat '${cfg.policy-agent.auth.secretIdFilePath}')"
+
+ seal_status=$(curl -s "$VAULT_ADDR/v1/sys/seal-status" | jq ".sealed")
+
+ echo "Seal Status: $seal_status"
+
+ if [ seal_status = "true" ]; then
+ echo "Vault is currently sealed, cannot install policies."
+ exit 1
+ fi
+
+ echo "Getting token..."
+
+ token=$(vault write -field=token auth/approle/login \
+ role_id="$role_id" \
+ secret_id="$secret_id" \
+ )
+
+ echo "Logging in..."
+
+ export VAULT_TOKEN="$(vault login -method=token -token-only token="$token")"
+
+ echo "Writing policies..."
+
+ ${write-policies}
+
+ ${optionalString (!cfg.mutable-policies) remove-unknown-policies}
+ '';
+ };
+ };
+}
diff --git a/modules/nixos/services/writefreely/default.nix b/modules/nixos/services/writefreely/default.nix
new file mode 100644
index 0000000..32a2424
--- /dev/null
+++ b/modules/nixos/services/writefreely/default.nix
@@ -0,0 +1,493 @@
+{ config, lib, pkgs, ... }:
+
+let
+ inherit (builtins) toString;
+ inherit (lib) types mkIf mkOption mkDefault;
+ inherit (lib) optional optionals optionalAttrs optionalString;
+
+ inherit (pkgs) sqlite;
+
+ format = pkgs.formats.ini {
+ mkKeyValue = key: value:
+ let
+ value' =
+ if builtins.isNull value then
+ ""
+ else if builtins.isBool value then
+ if value == true then "true" else "false"
+ else
+ toString value;
+ in
+ "${key} = ${value'}";
+ };
+
+ cfg = config.plusultra.services.writefreely;
+
+ isSqlite = cfg.database.type == "sqlite3";
+ isMysql = cfg.database.type == "mysql";
+ isMysqlLocal = isMysql && cfg.database.createLocally == true;
+
+ hostProtocol = if cfg.acme.enable then "https" else "http";
+
+ settings = cfg.settings // {
+ app = cfg.settings.app or { } // {
+ host = cfg.settings.app.host or "${hostProtocol}://${cfg.host}";
+ };
+
+ database =
+ if cfg.database.type == "sqlite3" then {
+ type = "sqlite3";
+ filename = cfg.settings.database.filename or "writefreely.db";
+ database = cfg.database.name;
+ } else {
+ type = "mysql";
+ username = cfg.database.user;
+ password = "#dbpass#";
+ database = cfg.database.name;
+ host = cfg.database.host;
+ port = cfg.database.port;
+ tls = cfg.database.tls;
+ };
+
+ server = cfg.settings.server or { } // {
+ bind = cfg.settings.server.bind or "localhost";
+ gopher_port = cfg.settings.server.gopher_port or 0;
+ autocert = !cfg.nginx.enable && cfg.acme.enable;
+ templates_parent_dir =
+ cfg.settings.server.templates_parent_dir or cfg.package.src;
+ static_parent_dir = cfg.settings.server.static_parent_dir or assets;
+ pages_parent_dir =
+ cfg.settings.server.pages_parent_dir or cfg.package.src;
+ keys_parent_dir = cfg.settings.server.keys_parent_dir or cfg.stateDir;
+ };
+ };
+
+ configFile = format.generate "config.ini" settings;
+
+ assets = pkgs.stdenvNoCC.mkDerivation {
+ pname = "writefreely-assets";
+
+ inherit (cfg.package) version src;
+
+ nativeBuildInputs = with pkgs.nodePackages; [ less ];
+
+ buildPhase = ''
+ mkdir -p $out
+
+ cp -r static $out/
+ '';
+
+ installPhase = ''
+ less_dir=$src/less
+ css_dir=$out/static/css
+
+ lessc $less_dir/app.less $css_dir/write.css
+ lessc $less_dir/fonts.less $css_dir/fonts.css
+ lessc $less_dir/icons.less $css_dir/icons.css
+ lessc $less_dir/prose.less $css_dir/prose.css
+ '';
+ };
+
+ withConfigFile = text: ''
+ db_pass=${
+ optionalString (cfg.database.passwordFile != null)
+ "$(head -n1 ${cfg.database.passwordFile})"
+ }
+
+ cp -f ${configFile} '${cfg.stateDir}/config.ini'
+ sed -e "s,#dbpass#,$db_pass,g" -i '${cfg.stateDir}/config.ini'
+ chmod 440 '${cfg.stateDir}/config.ini'
+
+ ${text}
+ '';
+
+ withMysql = text:
+ withConfigFile ''
+ query () {
+ local result=$(${config.services.mysql.package}/bin/mysql \
+ --user=${cfg.database.user} \
+ --password=$db_pass \
+ --database=${cfg.database.name} \
+ --silent \
+ --raw \
+ --skip-column-names \
+ --execute "$1" \
+ )
+
+ echo $result
+ }
+
+ ${text}
+ '';
+
+ withSqlite = text:
+ withConfigFile ''
+ query () {
+ local result=$(${sqlite}/bin/sqlite3 \
+ '${cfg.stateDir}/${settings.database.filename}'
+ "$1" \
+ )
+
+ echo $result
+ }
+
+ ${text}
+ '';
+in
+{
+ options.plusultra.services.writefreely = {
+ enable =
+ lib.mkEnableOption (lib.mdDoc "Writefreely, build a digital writing community");
+
+ package = lib.mkOption {
+ type = lib.types.package;
+ default = pkgs.writefreely;
+ defaultText = lib.literalExpression "pkgs.writefreely";
+ description = lib.mdDoc "Writefreely package to use.";
+ };
+
+ stateDir = mkOption {
+ type = types.path;
+ default = "/var/lib/writefreely";
+ description = lib.mdDoc "The state directory where keys and data are stored.";
+ };
+
+ user = mkOption {
+ type = types.str;
+ default = "writefreely";
+ description = lib.mdDoc "User under which Writefreely is ran.";
+ };
+
+ group = mkOption {
+ type = types.str;
+ default = "writefreely";
+ description = lib.mdDoc "Group under which Writefreely is ran.";
+ };
+
+ host = mkOption {
+ type = types.str;
+ default = "";
+ description = lib.mdDoc "The public host name to serve.";
+ example = "example.com";
+ };
+
+ settings = mkOption {
+ default = { };
+ description = lib.mdDoc ''
+ Writefreely configuration (`config.ini`). Refer to
+ [writefreely.org/docs/latest/admin/config](https://writefreely.org/docs/latest/admin/config)
+ for details.
+ '';
+
+ type = types.submodule {
+ freeformType = format.type;
+
+ options = {
+ app = {
+ theme = mkOption {
+ type = types.str;
+ default = "write";
+ description = "The theme to apply.";
+ };
+ };
+
+ server = {
+ port = mkOption {
+ type = types.port;
+ default = if cfg.nginx.enable then 18080 else 80;
+ defaultText = "80";
+ description = "The port WriteFreely should listen on.";
+ };
+ };
+ };
+ };
+ };
+
+ database = {
+ type = mkOption {
+ type = types.enum [ "sqlite3" "mysql" ];
+ default = "sqlite3";
+ description = "The database provider to use.";
+ };
+
+ name = mkOption {
+ type = types.str;
+ default = "writefreely";
+ description = "The name of the database to store data in.";
+ };
+
+ user = mkOption {
+ type = types.nullOr types.str;
+ default = if cfg.database.type == "mysql" then "writefreely" else null;
+ defaultText = "writefreely";
+ description = lib.mdDoc "The database user to connect as.";
+ };
+
+ passwordFile = mkOption {
+ type = types.nullOr types.path;
+ default = null;
+ description = lib.mdDoc "The file to load the database password from.";
+ };
+
+ host = mkOption {
+ type = types.str;
+ default = "localhost";
+ description = lib.mdDoc "The database host to connect to.";
+ };
+
+ port = mkOption {
+ type = types.port;
+ default = 3306;
+ description = lib.mdDoc "The port used when connecting to the database host.";
+ };
+
+ tls = mkOption {
+ type = types.bool;
+ default = false;
+ description =
+ lib.mdDoc "Whether or not TLS should be used for the database connection.";
+ };
+
+ migrate = mkOption {
+ type = types.bool;
+ default = true;
+ description =
+ lib.mdDoc "Whether or not to automatically run migrations on startup.";
+ };
+
+ createLocally = mkOption {
+ type = types.bool;
+ default = false;
+ description = lib.mdDoc ''
+ When `plusultra.services.writefreely.database.type` is set to
+ `"mysql"`, this option will enable the MySQL service locally.
+ '';
+ };
+ };
+
+ admin = {
+ name = mkOption {
+ type = types.nullOr types.str;
+ description = "The name of the first admin user.";
+ default = null;
+ };
+
+ initialPasswordFile = mkOption {
+ type = types.path;
+ description = ''
+ Path to a file containing the initial password for the admin user.
+ If not provided, the default password will be set to nixos
.
+ '';
+ default = pkgs.writeText "default-admin-pass" "nixos";
+ defaultText = "/nix/store/xxx-default-admin-pass";
+ };
+ };
+
+ nginx = {
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ description =
+ "Whether or not to enable and configure nginx as a proxy for WriteFreely.";
+ };
+
+ forceSSL = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Whether or not to force the use of SSL.";
+ };
+ };
+
+ acme = {
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ description =
+ "Whether or not to automatically fetch and configure SSL certs.";
+ };
+ };
+ };
+
+ config = mkIf cfg.enable {
+ assertions = [
+ {
+ assertion = cfg.host != "";
+ message = "plusultra.services.writefreely.host must be set";
+ }
+ {
+ assertion = isMysqlLocal -> cfg.database.passwordFile != null;
+ message =
+ "plusultra.services.writefreely.database.passwordFile must be set if plusultra.services.writefreely.database.createLocally is set to true";
+ }
+ {
+ assertion = isSqlite -> !cfg.database.createLocally;
+ message =
+ "plusultra.services.writefreely.database.createLocally has no use when plusultra.services.writefreely.database.type is set to sqlite3";
+ }
+ ];
+
+ users = {
+ users = optionalAttrs (cfg.user == "writefreely") {
+ writefreely = {
+ group = cfg.group;
+ home = cfg.stateDir;
+ isSystemUser = true;
+ };
+ };
+
+ groups =
+ optionalAttrs (cfg.group == "writefreely") { writefreely = { }; };
+ };
+
+ systemd.tmpfiles.rules =
+ [ "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" ];
+
+ systemd.services.writefreely = {
+ after = [ "network.target" ]
+ ++ optional isSqlite "writefreely-sqlite-init.service"
+ ++ optional isMysql "writefreely-mysql-init.service"
+ ++ optional isMysqlLocal "mysql.service";
+ wantedBy = [ "multi-user.target" ];
+
+ serviceConfig = {
+ Type = "simple";
+ User = cfg.user;
+ Group = cfg.group;
+ WorkingDirectory = cfg.stateDir;
+ Restart = "always";
+ RestartSec = 20;
+ ExecStart =
+ "${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' serve";
+ AmbientCapabilities =
+ optionalString (settings.server.port < 1024) "cap_net_bind_service";
+ };
+
+ preStart = ''
+ if ! test -d "${cfg.stateDir}/keys"; then
+ mkdir -p ${cfg.stateDir}/keys
+
+ # Key files end up with the wrong permissions by default.
+ # We need to correct them so that Writefreely can read them.
+ chmod -R 750 "${cfg.stateDir}/keys"
+
+ ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' keys generate
+ fi
+ '';
+ };
+
+ systemd.services.writefreely-sqlite-init = mkIf isSqlite {
+ wantedBy = [ "multi-user.target" ];
+
+ serviceConfig = {
+ Type = "oneshot";
+ User = cfg.user;
+ Group = cfg.group;
+ WorkingDirectory = cfg.stateDir;
+ ReadOnlyPaths = optional (cfg.admin.initialPasswordFile != null)
+ cfg.admin.initialPasswordFile;
+ };
+
+ script =
+ let
+ migrateDatabase = optionalString cfg.database.migrate ''
+ ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db migrate
+ '';
+
+ createAdmin = optionalString (cfg.admin.name != null) ''
+ if [[ $(query "SELECT COUNT(*) FROM users") == 0 ]]; then
+ admin_pass=$(head -n1 ${cfg.admin.initialPasswordFile})
+
+ ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' --create-admin ${cfg.admin.name}:$admin_pass
+ fi
+ '';
+ in
+ withSqlite ''
+ if ! test -f '${settings.database.filename}'; then
+ ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db init
+ fi
+
+ ${migrateDatabase}
+
+ ${createAdmin}
+ '';
+ };
+
+ systemd.services.writefreely-mysql-init = mkIf isMysql {
+ wantedBy = [ "multi-user.target" ];
+ after = optional isMysqlLocal "mysql.service";
+
+ serviceConfig = {
+ Type = "oneshot";
+ User = cfg.user;
+ Group = cfg.group;
+ WorkingDirectory = cfg.stateDir;
+ ReadOnlyPaths = optional isMysqlLocal cfg.database.passwordFile
+ ++ optional (cfg.admin.initialPasswordFile != null)
+ cfg.admin.initialPasswordFile;
+ };
+
+ script =
+ let
+ updateUser = optionalString isMysqlLocal ''
+ # WriteFreely currently *requires* a password for authentication, so we
+ # need to update the user in MySQL accordingly. By default MySQL users
+ # authenticate with auth_socket or unix_socket.
+ # See: https://github.com/writefreely/writefreely/issues/568
+ ${config.services.mysql.package}/bin/mysql --skip-column-names --execute "ALTER USER '${cfg.database.user}'@'localhost' IDENTIFIED VIA unix_socket OR mysql_native_password USING PASSWORD('$db_pass'); FLUSH PRIVILEGES;"
+ '';
+
+ migrateDatabase = optionalString cfg.database.migrate ''
+ ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db migrate
+ '';
+
+ createAdmin = optionalString (cfg.admin.name != null) ''
+ if [[ $(query 'SELECT COUNT(*) FROM users') == 0 ]]; then
+ admin_pass=$(head -n1 ${cfg.admin.initialPasswordFile})
+ ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' --create-admin ${cfg.admin.name}:$admin_pass
+ fi
+ '';
+ in
+ withMysql ''
+ ${updateUser}
+
+ if [[ $(query "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '${cfg.database.name}'") == 0 ]]; then
+ ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db init
+ fi
+
+ ${migrateDatabase}
+
+ ${createAdmin}
+ '';
+ };
+
+ services.mysql = mkIf isMysqlLocal {
+ enable = true;
+ package = mkDefault pkgs.mariadb;
+ ensureDatabases = [ cfg.database.name ];
+ ensureUsers = [{
+ name = cfg.database.user;
+ ensurePermissions = {
+ "${cfg.database.name}.*" = "ALL PRIVILEGES";
+ # WriteFreely requires the use of passwords, so we need permissions
+ # to `ALTER` the user to add password support and also to reload
+ # permissions so they can be used.
+ "*.*" = "CREATE USER, RELOAD";
+ };
+ }];
+ };
+
+ services.nginx = lib.mkIf cfg.nginx.enable {
+ enable = true;
+ recommendedProxySettings = true;
+
+ virtualHosts."${cfg.host}" = {
+ enableACME = cfg.acme.enable;
+ forceSSL = cfg.nginx.forceSSL;
+
+ locations."/" = {
+ proxyPass = "http://127.0.0.1:${toString settings.server.port}";
+ };
+ };
+ };
+ };
+}
diff --git a/modules/nixos/suites/art/default.nix b/modules/nixos/suites/art/default.nix
new file mode 100644
index 0000000..256341f
--- /dev/null
+++ b/modules/nixos/suites/art/default.nix
@@ -0,0 +1,22 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.suites.art;
+in
+{
+ options.plusultra.suites.art = with types; {
+ enable = mkBoolOpt false "Whether or not to enable art configuration.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra = {
+ apps = {
+ gimp = enabled;
+ inkscape = enabled;
+ blender = enabled;
+ };
+ };
+ };
+}
diff --git a/modules/nixos/suites/business/default.nix b/modules/nixos/suites/business/default.nix
new file mode 100644
index 0000000..aa5a3a4
--- /dev/null
+++ b/modules/nixos/suites/business/default.nix
@@ -0,0 +1,15 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.suites.business;
+in
+{
+ options.plusultra.suites.business = with types; {
+ enable = mkBoolOpt false "Whether or not to enable business configuration.";
+ };
+
+ config =
+ mkIf cfg.enable { plusultra = { apps = { frappe-books = enabled; }; }; };
+}
diff --git a/modules/nixos/suites/common-slim/default.nix b/modules/nixos/suites/common-slim/default.nix
new file mode 100644
index 0000000..466bb13
--- /dev/null
+++ b/modules/nixos/suites/common-slim/default.nix
@@ -0,0 +1,63 @@
+{ options
+, config
+, lib
+, pkgs
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.suites.common-slim;
+in
+{
+ options.plusultra.suites.common-slim = with types; {
+ enable = mkBoolOpt false "Whether or not to enable common-slim configuration.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = [
+ pkgs.plusultra.list-iommu
+ ];
+
+ plusultra = {
+ nix = enabled;
+
+ # TODO: Enable this once Attic is configured again.
+ # cache.public = enabled;
+
+ cli-apps = {
+ flake = enabled;
+ thaw = enabled;
+ };
+
+ tools = {
+ git = enabled;
+ fup-repl = enabled;
+ comma = enabled;
+ bottom = enabled;
+ direnv = enabled;
+ };
+
+ hardware = {
+ storage = enabled;
+ networking = enabled;
+ };
+
+ services = {
+ openssh = enabled;
+ tailscale = enabled;
+ };
+
+ security = {
+ doas = enabled;
+ };
+
+ system = {
+ boot = enabled;
+ fonts = enabled;
+ locale = enabled;
+ time = enabled;
+ xkb = enabled;
+ };
+ };
+ };
+}
diff --git a/modules/nixos/suites/common/default.nix b/modules/nixos/suites/common/default.nix
new file mode 100644
index 0000000..993e6ca
--- /dev/null
+++ b/modules/nixos/suites/common/default.nix
@@ -0,0 +1,68 @@
+{ options
+, config
+, lib
+, pkgs
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.suites.common;
+in
+{
+ options.plusultra.suites.common = with types; {
+ enable = mkBoolOpt false "Whether or not to enable common configuration.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = [
+ pkgs.plusultra.list-iommu
+ ];
+
+ plusultra = {
+ nix = enabled;
+
+ # TODO: Enable this once Attic is configured again.
+ # cache.public = enabled;
+
+ cli-apps = {
+ flake = enabled;
+ thaw = enabled;
+ };
+
+ tools = {
+ git = enabled;
+ misc = enabled;
+ fup-repl = enabled;
+ comma = enabled;
+ nix-ld = enabled;
+ bottom = enabled;
+ };
+
+ hardware = {
+ audio = enabled;
+ storage = enabled;
+ networking = enabled;
+ };
+
+ services = {
+ printing = enabled;
+ openssh = enabled;
+ tailscale = enabled;
+ };
+
+ security = {
+ gpg = enabled;
+ doas = enabled;
+ keyring = enabled;
+ };
+
+ system = {
+ boot = enabled;
+ fonts = enabled;
+ locale = enabled;
+ time = enabled;
+ xkb = enabled;
+ };
+ };
+ };
+}
diff --git a/modules/nixos/suites/desktop/default.nix b/modules/nixos/suites/desktop/default.nix
new file mode 100644
index 0000000..d8720af
--- /dev/null
+++ b/modules/nixos/suites/desktop/default.nix
@@ -0,0 +1,38 @@
+{ options
+, config
+, lib
+, pkgs
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.suites.desktop;
+in
+{
+ options.plusultra.suites.desktop = with types; {
+ enable =
+ mkBoolOpt false "Whether or not to enable common desktop configuration.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra = {
+ desktop = {
+ gnome = enabled;
+
+ addons = { wallpapers = enabled; };
+ };
+
+ apps = {
+ _1password = enabled;
+ firefox = enabled;
+ vlc = enabled;
+ logseq = enabled;
+ hey = enabled;
+ pocketcasts = enabled;
+ yt-music = enabled;
+ twitter = enabled;
+ gparted = enabled;
+ };
+ };
+ };
+}
diff --git a/modules/nixos/suites/development/default.nix b/modules/nixos/suites/development/default.nix
new file mode 100644
index 0000000..8929984
--- /dev/null
+++ b/modules/nixos/suites/development/default.nix
@@ -0,0 +1,55 @@
+{ options
+, config
+, lib
+, pkgs
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.suites.development;
+ apps = {
+ vscode = enabled;
+ yubikey = enabled;
+ };
+ cli-apps = {
+ tmux = enabled;
+ neovim = enabled;
+ yubikey = enabled;
+ prisma = enabled;
+ };
+in
+{
+ options.plusultra.suites.development = with types; {
+ enable =
+ mkBoolOpt false
+ "Whether or not to enable common development configuration.";
+ };
+
+ config = mkIf cfg.enable {
+ networking.firewall.allowedTCPPorts = [
+ 12345
+ 3000
+ 3001
+ 8080
+ 8081
+ ];
+
+ plusultra = {
+ inherit apps cli-apps;
+
+ tools = {
+ # attic = enabled;
+ at = enabled;
+ direnv = enabled;
+ go = enabled;
+ http = enabled;
+ k8s = enabled;
+ node = enabled;
+ titan = enabled;
+ qmk = enabled;
+ };
+
+ virtualisation = { podman = enabled; };
+ };
+ };
+}
diff --git a/modules/nixos/suites/emulation/default.nix b/modules/nixos/suites/emulation/default.nix
new file mode 100644
index 0000000..7665b16
--- /dev/null
+++ b/modules/nixos/suites/emulation/default.nix
@@ -0,0 +1,23 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.suites.emulation;
+in
+{
+ options.plusultra.suites.emulation = with types; {
+ enable =
+ mkBoolOpt false "Whether or not to enable emulation configuration.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra = {
+ apps = {
+ yuzu = enabled;
+ pcsx2 = enabled;
+ dolphin = enabled;
+ };
+ };
+ };
+}
diff --git a/modules/nixos/suites/games/default.nix b/modules/nixos/suites/games/default.nix
new file mode 100644
index 0000000..0e84ba0
--- /dev/null
+++ b/modules/nixos/suites/games/default.nix
@@ -0,0 +1,28 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.suites.games;
+ apps = {
+ steam = enabled;
+ prismlauncher = enabled;
+ lutris = enabled;
+ winetricks = enabled;
+ protontricks = enabled;
+ doukutsu-rs = enabled;
+ bottles = enabled;
+ };
+ cli-apps = {
+ wine = enabled;
+ proton = enabled;
+ };
+in
+{
+ options.plusultra.suites.games = with types; {
+ enable =
+ mkBoolOpt false "Whether or not to enable common games configuration.";
+ };
+
+ config = mkIf cfg.enable { plusultra = { inherit apps cli-apps; }; };
+}
diff --git a/modules/nixos/suites/media/default.nix b/modules/nixos/suites/media/default.nix
new file mode 100644
index 0000000..4b18918
--- /dev/null
+++ b/modules/nixos/suites/media/default.nix
@@ -0,0 +1,14 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.suites.media;
+in
+{
+ options.plusultra.suites.media = with types; {
+ enable = mkBoolOpt false "Whether or not to enable media configuration.";
+ };
+
+ config = mkIf cfg.enable { plusultra = { apps = { freetube = enabled; }; }; };
+}
diff --git a/modules/nixos/suites/music/default.nix b/modules/nixos/suites/music/default.nix
new file mode 100644
index 0000000..1715e8b
--- /dev/null
+++ b/modules/nixos/suites/music/default.nix
@@ -0,0 +1,21 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.suites.music;
+in
+{
+ options.plusultra.suites.music = with types; {
+ enable = mkBoolOpt false "Whether or not to enable music configuration.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra = {
+ apps = {
+ ardour = enabled;
+ bottles = enabled;
+ };
+ };
+ };
+}
diff --git a/modules/nixos/suites/social/default.nix b/modules/nixos/suites/social/default.nix
new file mode 100644
index 0000000..7923897
--- /dev/null
+++ b/modules/nixos/suites/social/default.nix
@@ -0,0 +1,27 @@
+{ options
+, config
+, lib
+, pkgs
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.suites.social;
+in
+{
+ options.plusultra.suites.social = with types; {
+ enable = mkBoolOpt false "Whether or not to enable social configuration.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra = {
+ apps = {
+ discord = {
+ enable = true;
+ chromium = enabled;
+ };
+ element = enabled;
+ };
+ };
+ };
+}
diff --git a/modules/nixos/suites/video/default.nix b/modules/nixos/suites/video/default.nix
new file mode 100644
index 0000000..080bf10
--- /dev/null
+++ b/modules/nixos/suites/video/default.nix
@@ -0,0 +1,21 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.suites.video;
+in
+{
+ options.plusultra.suites.video = with types; {
+ enable = mkBoolOpt false "Whether or not to enable video configuration.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra = {
+ apps = {
+ pitivi = enabled;
+ obs = enabled;
+ };
+ };
+ };
+}
diff --git a/modules/nixos/system/boot/default.nix b/modules/nixos/system/boot/default.nix
new file mode 100644
index 0000000..587d66a
--- /dev/null
+++ b/modules/nixos/system/boot/default.nix
@@ -0,0 +1,24 @@
+{ options
+, config
+, pkgs
+, lib
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.system.boot;
+in
+{
+ options.plusultra.system.boot = with types; {
+ enable = mkBoolOpt false "Whether or not to enable booting.";
+ };
+
+ config = mkIf cfg.enable {
+ boot.loader.systemd-boot.enable = true;
+ boot.loader.systemd-boot.configurationLimit = 10;
+ boot.loader.efi.canTouchEfiVariables = true;
+
+ # https://github.com/NixOS/nixpkgs/blob/c32c39d6f3b1fe6514598fa40ad2cf9ce22c3fb7/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix#L66
+ boot.loader.systemd-boot.editor = false;
+ };
+}
diff --git a/modules/nixos/system/env/default.nix b/modules/nixos/system/env/default.nix
new file mode 100644
index 0000000..d98732e
--- /dev/null
+++ b/modules/nixos/system/env/default.nix
@@ -0,0 +1,39 @@
+{ options, config, pkgs, lib, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.system.env;
+in
+{
+ options.plusultra.system.env = with types;
+ mkOption {
+ type = attrsOf (oneOf [ str path (listOf (either str path)) ]);
+ apply = mapAttrs (n: v:
+ if isList v then
+ concatMapStringsSep ":" (x: toString x) v
+ else
+ (toString v));
+ default = { };
+ description = "A set of environment variables to set.";
+ };
+
+ config = {
+ environment = {
+ sessionVariables = {
+ XDG_CACHE_HOME = "$HOME/.cache";
+ XDG_CONFIG_HOME = "$HOME/.config";
+ XDG_DATA_HOME = "$HOME/.local/share";
+ XDG_BIN_HOME = "$HOME/.local/bin";
+ # To prevent firefox from creating ~/Desktop.
+ XDG_DESKTOP_DIR = "$HOME";
+ };
+ variables = {
+ # Make some programs "XDG" compliant.
+ LESSHISTFILE = "$XDG_CACHE_HOME/less.history";
+ WGETRC = "$XDG_CONFIG_HOME/wgetrc";
+ };
+ extraInit = concatStringsSep "\n"
+ (mapAttrsToList (n: v: ''export ${n}="${v}"'') cfg);
+ };
+ };
+}
diff --git a/modules/nixos/system/fonts/default.nix b/modules/nixos/system/fonts/default.nix
new file mode 100644
index 0000000..e254d91
--- /dev/null
+++ b/modules/nixos/system/fonts/default.nix
@@ -0,0 +1,35 @@
+{ options
+, config
+, pkgs
+, lib
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.system.fonts;
+in
+{
+ options.plusultra.system.fonts = with types; {
+ enable = mkBoolOpt false "Whether or not to manage fonts.";
+ fonts = mkOpt (listOf package) [ ] "Custom font packages to install.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.variables = {
+ # Enable icons in tooling since we have nerdfonts.
+ LOG_ICONS = "true";
+ };
+
+ environment.systemPackages = with pkgs; [ font-manager ];
+
+ fonts.packages = with pkgs;
+ [
+ noto-fonts
+ noto-fonts-cjk-sans
+ noto-fonts-cjk-serif
+ noto-fonts-emoji
+ (nerdfonts.override { fonts = [ "Hack" ]; })
+ ]
+ ++ cfg.fonts;
+ };
+}
diff --git a/modules/nixos/system/locale/default.nix b/modules/nixos/system/locale/default.nix
new file mode 100644
index 0000000..5887920
--- /dev/null
+++ b/modules/nixos/system/locale/default.nix
@@ -0,0 +1,17 @@
+{ options, config, pkgs, lib, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.system.locale;
+in
+{
+ options.plusultra.system.locale = with types; {
+ enable = mkBoolOpt false "Whether or not to manage locale settings.";
+ };
+
+ config = mkIf cfg.enable {
+ i18n.defaultLocale = "en_US.UTF-8";
+
+ console = { keyMap = mkForce "us"; };
+ };
+}
diff --git a/modules/nixos/system/time/default.nix b/modules/nixos/system/time/default.nix
new file mode 100644
index 0000000..027e8ba
--- /dev/null
+++ b/modules/nixos/system/time/default.nix
@@ -0,0 +1,14 @@
+{ options, config, pkgs, lib, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.system.time;
+in
+{
+ options.plusultra.system.time = with types; {
+ enable =
+ mkBoolOpt false "Whether or not to configure timezone information.";
+ };
+
+ config = mkIf cfg.enable { time.timeZone = "America/Los_Angeles"; };
+}
diff --git a/modules/nixos/system/xkb/default.nix b/modules/nixos/system/xkb/default.nix
new file mode 100644
index 0000000..a6bef3a
--- /dev/null
+++ b/modules/nixos/system/xkb/default.nix
@@ -0,0 +1,19 @@
+{ options, config, lib, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.system.xkb;
+in
+{
+ options.plusultra.system.xkb = with types; {
+ enable = mkBoolOpt false "Whether or not to configure xkb.";
+ };
+
+ config = mkIf cfg.enable {
+ console.useXkbConfig = true;
+ services.xserver = {
+ layout = "us";
+ xkbOptions = "caps:escape";
+ };
+ };
+}
diff --git a/modules/nixos/system/zfs/default.nix b/modules/nixos/system/zfs/default.nix
new file mode 100644
index 0000000..4030b30
--- /dev/null
+++ b/modules/nixos/system/zfs/default.nix
@@ -0,0 +1,47 @@
+{ config, lib, ... }:
+
+let
+ cfg = config.plusultra.system.zfs;
+
+ inherit (lib) mkEnableOption mkIf mkDefault;
+ inherit (lib.plusultra) mkOpt enabled;
+ inherit (lib.types) listOf str;
+in
+{
+ options.plusultra.system.zfs = {
+ enable = mkEnableOption "ZFS support";
+
+ pools = mkOpt (listOf str) [ "rpool" ] "The ZFS pools to manage.";
+
+ auto-snapshot = {
+ enable = mkEnableOption "ZFS auto snapshotting";
+ };
+ };
+
+ config = mkIf cfg.enable {
+ boot.kernelPackages = config.boot.zfs.package.latestCompatibleLinuxPackages;
+
+ services.zfs = {
+ autoScrub = {
+ enable = true;
+ pools = cfg.pools;
+ };
+
+ autoSnapshot = mkIf cfg.auto-snapshot.enable {
+ enable = true;
+ flags = "-k -p --utc";
+ weekly = mkDefault 3;
+ daily = mkDefault 3;
+ hourly = mkDefault 0;
+ frequent = mkDefault 0;
+ monthly = mkDefault 2;
+ };
+ };
+
+ plusultra = {
+ tools = {
+ icehouse = enabled;
+ };
+ };
+ };
+}
diff --git a/modules/nixos/tools/appimage-run/default.nix b/modules/nixos/tools/appimage-run/default.nix
new file mode 100644
index 0000000..67a4420
--- /dev/null
+++ b/modules/nixos/tools/appimage-run/default.nix
@@ -0,0 +1,20 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.tools.appimage-run;
+in
+{
+ options.plusultra.tools.appimage-run = with types; {
+ enable = mkBoolOpt false "Whether or not to enable appimage-run.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra.home.configFile."wgetrc".text = "";
+
+ environment.systemPackages = with pkgs; [
+ appimage-run
+ ];
+ };
+}
diff --git a/modules/nixos/tools/at/default.nix b/modules/nixos/tools/at/default.nix
new file mode 100644
index 0000000..fdf61c3
--- /dev/null
+++ b/modules/nixos/tools/at/default.nix
@@ -0,0 +1,19 @@
+{ options, config, pkgs, lib, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.tools.at;
+in
+{
+ options.plusultra.tools.at = with types; {
+ enable = mkBoolOpt false "Whether or not to install at.";
+ pkg = mkOpt package pkgs.plusultra.at "The package to install as at.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = [
+ cfg.pkg
+ ];
+ };
+}
diff --git a/modules/nixos/tools/attic/default.nix b/modules/nixos/tools/attic/default.nix
new file mode 100644
index 0000000..94efa85
--- /dev/null
+++ b/modules/nixos/tools/attic/default.nix
@@ -0,0 +1,18 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.tools.attic;
+in
+{
+ options.plusultra.tools.attic = {
+ enable = mkEnableOption "Attic";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [
+ attic
+ ];
+ };
+}
diff --git a/modules/nixos/tools/bottom/default.nix b/modules/nixos/tools/bottom/default.nix
new file mode 100644
index 0000000..287f641
--- /dev/null
+++ b/modules/nixos/tools/bottom/default.nix
@@ -0,0 +1,18 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.tools.bottom;
+in
+{
+ options.plusultra.tools.bottom = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Bottom.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [
+ bottom
+ ];
+ };
+}
diff --git a/modules/nixos/tools/comma/default.nix b/modules/nixos/tools/comma/default.nix
new file mode 100644
index 0000000..b9cc77c
--- /dev/null
+++ b/modules/nixos/tools/comma/default.nix
@@ -0,0 +1,29 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.tools.comma;
+in
+{
+ options.plusultra.tools.comma = with types; {
+ enable = mkBoolOpt false "Whether or not to enable comma.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [
+ comma
+ plusultra.nix-update-index
+ ];
+
+ plusultra.home = {
+ configFile = {
+ "wgetrc".text = "";
+ };
+
+ extraOptions = {
+ programs.nix-index.enable = true;
+ };
+ };
+ };
+}
diff --git a/modules/nixos/tools/direnv/default.nix b/modules/nixos/tools/direnv/default.nix
new file mode 100644
index 0000000..f09398c
--- /dev/null
+++ b/modules/nixos/tools/direnv/default.nix
@@ -0,0 +1,20 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.tools.direnv;
+in
+{
+ options.plusultra.tools.direnv = with types; {
+ enable = mkBoolOpt false "Whether or not to enable direnv.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra.home.extraOptions = {
+ programs.direnv = {
+ enable = true;
+ nix-direnv = enabled;
+ };
+ };
+ };
+}
diff --git a/modules/nixos/tools/fup-repl/default.nix b/modules/nixos/tools/fup-repl/default.nix
new file mode 100644
index 0000000..52c01ab
--- /dev/null
+++ b/modules/nixos/tools/fup-repl/default.nix
@@ -0,0 +1,17 @@
+{ lib, pkgs, config, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.tools.fup-repl;
+ fup-repl = pkgs.writeShellScriptBin "fup-repl" ''
+ ${pkgs.fup-repl}/bin/repl ''${@}
+ '';
+in
+{
+ options.plusultra.tools.fup-repl = with types; {
+ enable = mkBoolOpt false "Whether to enable fup-repl or not";
+ };
+
+ config = mkIf cfg.enable { environment.systemPackages = [ fup-repl ]; };
+}
diff --git a/modules/nixos/tools/git/default.nix b/modules/nixos/tools/git/default.nix
new file mode 100644
index 0000000..c13ba67
--- /dev/null
+++ b/modules/nixos/tools/git/default.nix
@@ -0,0 +1,43 @@
+{ options, config, pkgs, lib, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.tools.git;
+ gpg = config.plusultra.security.gpg;
+ user = config.plusultra.user;
+in
+{
+ options.plusultra.tools.git = with types; {
+ enable = mkBoolOpt false "Whether or not to install and configure git.";
+ userName = mkOpt types.str user.fullName "The name to configure git with.";
+ userEmail = mkOpt types.str user.email "The email to configure git with.";
+ signingKey =
+ mkOpt types.str "9762169A1B35EA68" "The key ID to sign commits with.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [ git ];
+
+ plusultra.home.extraOptions = {
+ programs.git = {
+ enable = true;
+ inherit (cfg) userName userEmail;
+ lfs = enabled;
+ signing = {
+ key = cfg.signingKey;
+ signByDefault = mkIf gpg.enable true;
+ };
+ extraConfig = {
+ init = { defaultBranch = "main"; };
+ pull = { rebase = true; };
+ push = { autoSetupRemote = true; };
+ core = { whitespace = "trailing-space,space-before-tab"; };
+ safe = {
+ directory = "${config.users.users.${user.name}.home}/work/config";
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/modules/nixos/tools/go/default.nix b/modules/nixos/tools/go/default.nix
new file mode 100644
index 0000000..923c027
--- /dev/null
+++ b/modules/nixos/tools/go/default.nix
@@ -0,0 +1,21 @@
+{ options, config, pkgs, lib, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.tools.go;
+in
+{
+ options.plusultra.tools.go = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Go support.";
+ };
+
+ config = mkIf cfg.enable {
+ environment = {
+ systemPackages = with pkgs; [ go gopls ];
+ sessionVariables = {
+ GOPATH = "$HOME/work/go";
+ };
+ };
+ };
+}
diff --git a/modules/nixos/tools/http/default.nix b/modules/nixos/tools/http/default.nix
new file mode 100644
index 0000000..0094c31
--- /dev/null
+++ b/modules/nixos/tools/http/default.nix
@@ -0,0 +1,14 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.tools.http;
+in
+{
+ options.plusultra.tools.http = with types; {
+ enable = mkBoolOpt false "Whether or not to enable common http utilities.";
+ };
+
+ config =
+ mkIf cfg.enable { environment.systemPackages = with pkgs; [ wget curl ]; };
+}
diff --git a/modules/nixos/tools/icehouse/default.nix b/modules/nixos/tools/icehouse/default.nix
new file mode 100644
index 0000000..dc58825
--- /dev/null
+++ b/modules/nixos/tools/icehouse/default.nix
@@ -0,0 +1,16 @@
+{ config, lib, pkgs, ... }:
+
+let
+ cfg = config.plusultra.tools.icehouse;
+
+ inherit (lib) mkEnableOption mkIf;
+in
+{
+ options.plusultra.tools.icehouse = {
+ enable = mkEnableOption "Icehouse";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = [ pkgs.snowfallorg.icehouse ];
+ };
+}
diff --git a/modules/nixos/tools/k8s/default.nix b/modules/nixos/tools/k8s/default.nix
new file mode 100644
index 0000000..b1cc8bf
--- /dev/null
+++ b/modules/nixos/tools/k8s/default.nix
@@ -0,0 +1,31 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.tools.k8s;
+in
+{
+ options.plusultra.tools.k8s = with types; {
+ enable =
+ mkBoolOpt false "Whether or not to enable common Kubernetes utilities.";
+ };
+
+ config = mkIf cfg.enable {
+ programs.zsh.shellAliases = {
+ k = "kubecolor";
+ kubectl = "kubecolor";
+ kc = "kubectx";
+ kn = "kubens";
+ ks = "kubeseal";
+ };
+
+ environment.systemPackages = with pkgs; [
+ kubectl
+ kubectx
+ kubeseal
+ kubecolor
+ kubernetes-helm
+ helmfile
+ ];
+ };
+}
diff --git a/modules/nixos/tools/misc/default.nix b/modules/nixos/tools/misc/default.nix
new file mode 100644
index 0000000..379c1ec
--- /dev/null
+++ b/modules/nixos/tools/misc/default.nix
@@ -0,0 +1,25 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.tools.misc;
+in
+{
+ options.plusultra.tools.misc = with types; {
+ enable = mkBoolOpt false "Whether or not to enable common utilities.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra.home.configFile."wgetrc".text = "";
+
+ environment.systemPackages = with pkgs; [
+ fzf
+ killall
+ unzip
+ file
+ jq
+ clac
+ wget
+ ];
+ };
+}
diff --git a/modules/nixos/tools/nix-ld/default.nix b/modules/nixos/tools/nix-ld/default.nix
new file mode 100644
index 0000000..c84c284
--- /dev/null
+++ b/modules/nixos/tools/nix-ld/default.nix
@@ -0,0 +1,15 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.tools.nix-ld;
+in
+{
+ options.plusultra.tools.nix-ld = with types; {
+ enable = mkBoolOpt false "Whether or not to enable nix-ld.";
+ };
+
+ config = mkIf cfg.enable {
+ programs.nix-ld.enable = true;
+ };
+}
diff --git a/modules/nixos/tools/node/default.nix b/modules/nixos/tools/node/default.nix
new file mode 100644
index 0000000..9a70afd
--- /dev/null
+++ b/modules/nixos/tools/node/default.nix
@@ -0,0 +1,42 @@
+{ options
+, config
+, pkgs
+, lib
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.tools.node;
+in
+{
+ options.plusultra.tools.node = with types; {
+ enable = mkBoolOpt false "Whether or not to install and configure git";
+ pkg = mkOpt package pkgs.nodejs "The NodeJS package to use";
+ prettier = {
+ enable = mkBoolOpt true "Whether or not to install Prettier";
+ pkg =
+ mkOpt package pkgs.nodePackages.prettier "The NodeJS package to use";
+ };
+ yarn = {
+ enable = mkBoolOpt true "Whether or not to install Yarn";
+ pkg = mkOpt package pkgs.nodePackages.yarn "The NodeJS package to use";
+ };
+ pnpm = {
+ enable = mkBoolOpt true "Whether or not to install Pnpm";
+ pkg = mkOpt package pkgs.nodePackages.pnpm "The NodeJS package to use";
+ };
+ flyctl = {
+ enable = mkBoolOpt true "Whether or not to install flyctl";
+ pkg = mkOpt package pkgs.flyctl "The flyctl package to use";
+ };
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs;
+ [ cfg.pkg ]
+ ++ (lib.optional cfg.prettier.enable cfg.prettier.pkg)
+ ++ (lib.optional cfg.yarn.enable cfg.yarn.pkg)
+ ++ (lib.optional cfg.pnpm.enable cfg.pnpm.pkg)
+ ++ (lib.optional cfg.flyctl.enable cfg.flyctl.pkg);
+ };
+}
diff --git a/modules/nixos/tools/qmk/default.nix b/modules/nixos/tools/qmk/default.nix
new file mode 100644
index 0000000..1f2d45e
--- /dev/null
+++ b/modules/nixos/tools/qmk/default.nix
@@ -0,0 +1,21 @@
+{ options, config, pkgs, lib, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.tools.qmk;
+in
+{
+ options.plusultra.tools.qmk = with types; {
+ enable = mkBoolOpt false "Whether or not to enable QMK";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [
+ qmk
+ ];
+
+ services.udev.packages = with pkgs; [
+ qmk-udev-rules
+ ];
+ };
+}
diff --git a/modules/nixos/tools/titan/default.nix b/modules/nixos/tools/titan/default.nix
new file mode 100644
index 0000000..9442da2
--- /dev/null
+++ b/modules/nixos/tools/titan/default.nix
@@ -0,0 +1,25 @@
+{ options, config, pkgs, lib, ... }:
+
+with lib;
+with lib.plusultra;
+let
+ cfg = config.plusultra.tools.titan;
+in
+{
+ options.plusultra.tools.titan = with types; {
+ enable = mkBoolOpt false "Whether or not to install Titan.";
+ pkg = mkOpt package pkgs.plusultra.titan "The package to install as Titan.";
+ };
+
+ config = mkIf cfg.enable {
+ plusultra.tools = {
+ # Titan depends on Node and Git
+ node = enabled;
+ git = enabled;
+ };
+
+ environment.systemPackages = [
+ cfg.pkg
+ ];
+ };
+}
diff --git a/modules/nixos/user/default.nix b/modules/nixos/user/default.nix
new file mode 100644
index 0000000..076d3ac
--- /dev/null
+++ b/modules/nixos/user/default.nix
@@ -0,0 +1,168 @@
+{ options
+, config
+, pkgs
+, lib
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.user;
+ defaultIconFileName = "profile.png";
+ defaultIcon = pkgs.stdenvNoCC.mkDerivation {
+ name = "default-icon";
+ src = ./. + "/${defaultIconFileName}";
+
+ dontUnpack = true;
+
+ installPhase = ''
+ cp $src $out
+ '';
+
+ passthru = { fileName = defaultIconFileName; };
+ };
+ propagatedIcon =
+ pkgs.runCommandNoCC "propagated-icon"
+ { passthru = { fileName = cfg.icon.fileName; }; }
+ ''
+ local target="$out/share/plusultra-icons/user/${cfg.name}"
+ mkdir -p "$target"
+
+ cp ${cfg.icon} "$target/${cfg.icon.fileName}"
+ '';
+in
+{
+ options.plusultra.user = with types; {
+ name = mkOpt str "short" "The name to use for the user account.";
+ fullName = mkOpt str "Jake Hamilton" "The full name of the user.";
+ email = mkOpt str "jake.hamilton@hey.com" "The email of the user.";
+ initialPassword =
+ mkOpt str "password"
+ "The initial password to use when the user is first created.";
+ icon =
+ mkOpt (nullOr package) defaultIcon
+ "The profile picture to use for the user.";
+ prompt-init = mkBoolOpt true "Whether or not to show an initial message when opening a new shell.";
+ extraGroups = mkOpt (listOf str) [ ] "Groups for the user to be assigned.";
+ extraOptions =
+ mkOpt attrs { }
+ (mdDoc "Extra options passed to `users.users.`.");
+ };
+
+ config = {
+ environment.systemPackages = with pkgs; [
+ cowsay
+ fortune
+ lolcat
+ plusultra.cowsay-plus
+ propagatedIcon
+ ];
+
+ programs.zsh = {
+ enable = true;
+ autosuggestions.enable = true;
+ histFile = "$XDG_CACHE_HOME/zsh.history";
+ };
+
+ plusultra.home = {
+ file = {
+ "Desktop/.keep".text = "";
+ "Documents/.keep".text = "";
+ "Downloads/.keep".text = "";
+ "Music/.keep".text = "";
+ "Pictures/.keep".text = "";
+ "Videos/.keep".text = "";
+ "work/.keep".text = "";
+ ".face".source = cfg.icon;
+ "Pictures/${
+ cfg.icon.fileName or (builtins.baseNameOf cfg.icon)
+ }".source =
+ cfg.icon;
+ };
+
+ extraOptions = {
+ home.shellAliases = {
+ lc = "${pkgs.colorls}/bin/colorls --sd";
+ lcg = "lc --gs";
+ lcl = "lc -1";
+ lclg = "lc -1 --gs";
+ lcu = "${pkgs.colorls}/bin/colorls -U";
+ lclu = "${pkgs.colorls}/bin/colorls -U -1";
+ };
+
+ programs = {
+ starship = {
+ enable = true;
+ settings = {
+ character = {
+ success_symbol = "[➜](bold green)";
+ error_symbol = "[✗](bold red) ";
+ vicmd_symbol = "[](bold blue) ";
+ };
+ };
+ };
+
+ zsh = {
+ enable = true;
+ enableCompletion = true;
+ enableAutosuggestions = true;
+ syntaxHighlighting.enable = true;
+
+ initExtra =
+ ''
+ # Fix an issue with tmux.
+ export KEYTIMEOUT=1
+
+ # Use vim bindings.
+ set -o vi
+
+ # Improved vim bindings.
+ source ${pkgs.zsh-vi-mode}/share/zsh-vi-mode/zsh-vi-mode.plugin.zsh
+ ''
+ + optionalString cfg.prompt-init ''
+ ${pkgs.toilet}/bin/toilet -f future "Plus Ultra" --gay
+ '';
+
+ shellAliases = {
+ say = "${pkgs.toilet}/bin/toilet -f pagga";
+ };
+
+ plugins = [
+ {
+ name = "zsh-nix-shell";
+ file = "nix-shell.plugin.zsh";
+ src = pkgs.fetchFromGitHub {
+ owner = "chisui";
+ repo = "zsh-nix-shell";
+ rev = "v0.4.0";
+ sha256 = "037wz9fqmx0ngcwl9az55fgkipb745rymznxnssr3rx9irb6apzg";
+ };
+ }
+ ];
+ };
+ };
+ };
+ };
+
+ users.users.${cfg.name} =
+ {
+ isNormalUser = true;
+
+ inherit (cfg) name initialPassword;
+
+ home = "/home/${cfg.name}";
+ group = "users";
+
+ shell = pkgs.zsh;
+
+ # Arbitrary user ID to use for the user. Since I only
+ # have a single user on my machines this won't ever collide.
+ # However, if you add multiple users you'll need to change this
+ # so each user has their own unique uid (or leave it out for the
+ # system to select).
+ uid = 1000;
+
+ extraGroups = [ ] ++ cfg.extraGroups;
+ }
+ // cfg.extraOptions;
+ };
+}
diff --git a/modules/nixos/user/profile.png b/modules/nixos/user/profile.png
new file mode 100644
index 0000000..5aee9a3
Binary files /dev/null and b/modules/nixos/user/profile.png differ
diff --git a/modules/nixos/virtualisation/kvm/default.nix b/modules/nixos/virtualisation/kvm/default.nix
new file mode 100644
index 0000000..896434d
--- /dev/null
+++ b/modules/nixos/virtualisation/kvm/default.nix
@@ -0,0 +1,108 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+with lib;
+with lib.plusultra; let
+ cfg = config.plusultra.virtualisation.kvm;
+ user = config.plusultra.user;
+in
+{
+ options.plusultra.virtualisation.kvm = with types; {
+ enable = mkBoolOpt false "Whether or not to enable KVM virtualisation.";
+ vfioIds =
+ mkOpt (listOf str) [ ]
+ "The hardware IDs to pass through to a virtual machine.";
+ platform =
+ mkOpt (enum [ "amd" "intel" ]) "amd"
+ "Which CPU platform the machine is using.";
+ # Use `machinectl` and then `machinectl status ` to
+ # get the unit "*.scope" of the virtual machine.
+ machineUnits =
+ mkOpt (listOf str) [ ]
+ "The systemd *.scope units to wait for before starting Scream.";
+ };
+
+ config = mkIf cfg.enable {
+ boot = {
+ kernelModules = [
+ "kvm-${cfg.platform}"
+ "vfio_virqfd"
+ "vfio_pci"
+ "vfio_iommu_type1"
+ "vfio"
+ ];
+ kernelParams = [
+ "${cfg.platform}_iommu=on"
+ "${cfg.platform}_iommu=pt"
+ "kvm.ignore_msrs=1"
+ # "vfio-pci.ids=${concatStringsSep "," cfg.vfioIds}"
+ ];
+ extraModprobeConfig =
+ optionalString (length cfg.vfioIds > 0)
+ ''
+ softdep amdgpu pre: vfio vfio-pci
+ options vfio-pci ids=${concatStringsSep "," cfg.vfioIds}
+ '';
+ };
+
+ systemd.tmpfiles.rules = [
+ "f /dev/shm/looking-glass 0660 ${user.name} qemu-libvirtd -"
+ "f /dev/shm/scream 0660 ${user.name} qemu-libvirtd -"
+ ];
+
+ environment.systemPackages = with pkgs; [
+ virt-manager
+ ];
+
+ virtualisation = {
+ libvirtd = {
+ enable = true;
+ extraConfig = ''
+ user="${user.name}"
+ '';
+
+ onBoot = "ignore";
+ onShutdown = "shutdown";
+
+ qemu = {
+ package = pkgs.qemu_kvm;
+ ovmf = enabled;
+ swtpm = enabled;
+ verbatimConfig = ''
+ namespaces = []
+ user = "+${builtins.toString config.users.users.${user.name}.uid}"
+ '';
+ };
+ };
+ };
+
+ plusultra = {
+ user = { extraGroups = [ "qemu-libvirtd" "libvirtd" "disk" ]; };
+
+ apps = { looking-glass-client = enabled; };
+
+ home = {
+ extraOptions = {
+ systemd.user.services.scream = {
+ Unit.Description = "Scream";
+ Unit.After =
+ [
+ "libvirtd.service"
+ "pipewire-pulse.service"
+ "pipewire.service"
+ "sound.target"
+ ]
+ ++ cfg.machineUnits;
+ Service.ExecStart = "${pkgs.scream}/bin/scream -n scream -o pulse -m /dev/shm/scream";
+ Service.Restart = "always";
+ Service.StartLimitIntervalSec = "5";
+ Service.StartLimitBurst = "1";
+ Install.RequiredBy = cfg.machineUnits;
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/modules/nixos/virtualisation/podman/default.nix b/modules/nixos/virtualisation/podman/default.nix
new file mode 100644
index 0000000..80e7d2b
--- /dev/null
+++ b/modules/nixos/virtualisation/podman/default.nix
@@ -0,0 +1,32 @@
+{ options, config, lib, pkgs, ... }:
+
+with lib;
+with lib.plusultra;
+let cfg = config.plusultra.virtualisation.podman;
+in
+{
+ options.plusultra.virtualisation.podman = with types; {
+ enable = mkBoolOpt false "Whether or not to enable Podman.";
+ };
+
+ config = mkIf cfg.enable {
+ environment.systemPackages = with pkgs; [ podman-compose ];
+
+ plusultra.home.extraOptions = {
+ home.shellAliases = { "docker-compose" = "podman-compose"; };
+ };
+
+ # NixOS 22.05 moved NixOS Containers to a new state directory and the old
+ # directory is taken over by OCI Containers (eg. podman). For systems with
+ # system.stateVersion < 22.05, it is not possible to have both enabled.
+ # This option disables NixOS Containers, leaving OCI Containers available.
+ boot.enableContainers = false;
+
+ virtualisation = {
+ podman = {
+ enable = cfg.enable;
+ dockerCompat = true;
+ };
+ };
+ };
+}