about summary refs log tree commit diff stats
path: root/modules/system/services
diff options
context:
space:
mode:
Diffstat (limited to 'modules/system/services')
-rw-r--r--modules/system/services/adb/default.nix22
-rw-r--r--modules/system/services/backup/default.nix49
-rw-r--r--modules/system/services/dconf/default.nix7
-rw-r--r--modules/system/services/default.nix19
-rw-r--r--modules/system/services/fwupd/default.nix14
-rw-r--r--modules/system/services/issue_file/default.nix38
-rw-r--r--modules/system/services/nix/default.nix53
-rw-r--r--modules/system/services/openssh/default.nix15
-rw-r--r--modules/system/services/postgresql/default.nix17
-rw-r--r--modules/system/services/printing/default.nix45
-rw-r--r--modules/system/services/scanning/default.nix25
-rw-r--r--modules/system/services/serverphone/certificates/ca.crt10
-rw-r--r--modules/system/services/serverphone/certificates/server.crt10
-rw-r--r--modules/system/services/serverphone/default.nix49
l---------modules/system/services/serverphone/keys/key_11
l---------modules/system/services/serverphone/keys/key_21
-rw-r--r--modules/system/services/snapper/default.nix53
-rw-r--r--modules/system/services/steam/default.nix23
-rw-r--r--modules/system/services/swaylock/default.nix4
-rw-r--r--modules/system/services/xdg/default.nix58
-rwxr-xr-xmodules/system/services/xdg/scripts/lf_wrapper.sh79
-rwxr-xr-xmodules/system/services/xdg/scripts/ranger_wrapper.sh68
22 files changed, 660 insertions, 0 deletions
diff --git a/modules/system/services/adb/default.nix b/modules/system/services/adb/default.nix
new file mode 100644
index 00000000..4055dbb1
--- /dev/null
+++ b/modules/system/services/adb/default.nix
@@ -0,0 +1,22 @@
+{
+  lib,
+  config,
+  ...
+}: let
+  cfg = config.soispha.adb;
+in {
+  options.soispha.adb = {
+    enable = lib.mkEnableOption "Android adb bridge";
+    user = lib.mkOption {
+      type = lib.types.str;
+      example = "soispha";
+      default = "soispha";
+      description = "Username to grant access to adb bridge";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    programs.adb.enable = true;
+    users.users."${cfg.user}".extraGroups = ["adbusers"];
+  };
+}
diff --git a/modules/system/services/backup/default.nix b/modules/system/services/backup/default.nix
new file mode 100644
index 00000000..705dcf23
--- /dev/null
+++ b/modules/system/services/backup/default.nix
@@ -0,0 +1,49 @@
+{
+  lib,
+  pkgs,
+  config,
+  ...
+}: let
+  backup-script = pkgs.writeShellScriptBin "backsnap" ''
+    ${pkgs.util-linux}/bin/mount --mkdir "/dev/disk/by-uuid/${cfg.backupDiskUuid}" "/run/media/${cfg.backupDiskUuid}";
+    ${pkgs.snap-sync-forked}/bin/snap-sync-forked --UUID "${cfg.backupDiskUuid}" --noconfirm;
+    ${pkgs.util-linux}/bin/umount "/run/media/${cfg.backupDiskUuid}";
+  '';
+
+  cfg = config.soispha.backup;
+in {
+  options.soispha.backup = {
+    enable = lib.mkEnableOption "backups with my forked snap-sync";
+    backupDiskUuid = lib.mkOption {
+      type = lib.types.str;
+      example = lib.literalExpression "d1d20ae7-3d8a-44da-86da-677dbbb10c89";
+      description = "The UUID of the backup disk";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd = {
+      services.backup = {
+        wantedBy = lib.mkForce [];
+        unitConfig = {
+          Description = "Backup the last snapshots of the persitent-storage subvolume.";
+        };
+        serviceConfig = {
+          Type = "oneshot";
+          ExecStart = "${backup-script}/bin/backsnap";
+        };
+      };
+
+      timers.backup = {
+        wantedBy = ["timers.target"];
+        unitConfig = {
+          Description = "Backup 15min after boot and every 8 hours";
+        };
+        timerConfig = {
+          OnBootSec = "15min";
+          OnUnitActiveSec = "8h";
+        };
+      };
+    };
+  };
+}
diff --git a/modules/system/services/dconf/default.nix b/modules/system/services/dconf/default.nix
new file mode 100644
index 00000000..f6598a9b
--- /dev/null
+++ b/modules/system/services/dconf/default.nix
@@ -0,0 +1,7 @@
+{...}: {
+  # needed to make home-manager play nice with some apps. See:
+  # https://nix-community.github.io/home-manager/index.xhtml#_why_do_i_get_an_error_message_about_literal_ca_desrt_dconf_literal_or_literal_dconf_service_literal
+  programs.dconf.enable = true;
+  # FIXME: This should also be parameterized. <2024-05-16>
+}
+# vim: nolinebreak nowrap textwidth=0
diff --git a/modules/system/services/default.nix b/modules/system/services/default.nix
new file mode 100644
index 00000000..76ef26e2
--- /dev/null
+++ b/modules/system/services/default.nix
@@ -0,0 +1,19 @@
+{...}: {
+  imports = [
+    #./serverphone
+    ./adb
+    ./backup
+    ./dconf
+    ./fwupd
+    ./issue_file
+    ./nix
+    ./openssh
+    ./postgresql
+    ./printing
+    ./scanning
+    ./snapper
+    ./steam
+    ./swaylock
+    ./xdg
+  ];
+}
diff --git a/modules/system/services/fwupd/default.nix b/modules/system/services/fwupd/default.nix
new file mode 100644
index 00000000..5ad4f467
--- /dev/null
+++ b/modules/system/services/fwupd/default.nix
@@ -0,0 +1,14 @@
+{
+  config,
+  lib,
+  ...
+}: let
+  cfg = config.soispha.services.fwupd;
+in {
+  options.soispha.services.fwupd = {
+    enable = lib.mkEnableOption "fwupd";
+  };
+  config = lib.mkIf cfg.enable {
+    services.fwupd.enable = true;
+  };
+}
diff --git a/modules/system/services/issue_file/default.nix b/modules/system/services/issue_file/default.nix
new file mode 100644
index 00000000..930be1d9
--- /dev/null
+++ b/modules/system/services/issue_file/default.nix
@@ -0,0 +1,38 @@
+{config, ...}: {
+  environment.etc.issue = {
+    # Friendly greeting on the virtual consoles.
+    text = ''
+      [?25l[?7l                                           
+                ▗▄▄▄       ▗▄▄▄▄    ▄▄▄▖         
+                ▜███▙       ▜███▙  ▟███▛         
+                 ▜███▙       ▜███▙▟███▛          
+                  ▜███▙       ▜██████▛           
+           ▟█████████████████▙ ▜████▛     ▟▙     
+          ▟███████████████████▙ ▜███▙    ▟██▙    
+                 ▄▄▄▄▖           ▜███▙  ▟███▛    
+                ▟███▛             ▜██▛ ▟███▛     
+               ▟███▛               ▜▛ ▟███▛      
+      ▟███████████▛                  ▟██████████▙
+      ▜██████████▛                  ▟███████████▛
+            ▟███▛ ▟▙               ▟███▛         
+           ▟███▛ ▟██▙             ▟███▛          
+          ▟███▛  ▜███▙           ▝▀▀▀▀           
+          ▜██▛    ▜███▙ ▜██████████████████▛     
+           ▜▛     ▟████▙ ▜████████████████▛      
+                 ▟██████▙       ▜███▙            
+                ▟███▛▜███▙       ▜███▙           
+               ▟███▛  ▜███▙       ▜███▙          
+               ▝▀▀▀    ▀▀▀▀▘       ▀▀▀▘          
+                                                 
+       NixOS ${config.system.nixos.label} 
+        --------------
+      
+        date: \d
+        time: \t
+        ipv4: \4
+        ipv6: \6
+        tty: \l
+      
+    '';
+  };
+}
diff --git a/modules/system/services/nix/default.nix b/modules/system/services/nix/default.nix
new file mode 100644
index 00000000..65fc7273
--- /dev/null
+++ b/modules/system/services/nix/default.nix
@@ -0,0 +1,53 @@
+{
+  pkgs,
+
+  # flakes
+  nixpkgs_as_input,
+  templates,
+  self,
+  ...
+}: {
+  nix = {
+    package = pkgs.nixVersions.latest;
+
+    # Disable nix channels  (this is a remnant of old days)
+    channel.enable = false;
+
+    registry = {
+      nixpkgs.flake = nixpkgs_as_input;
+      n.flake = self; # Otherwise the nixpkgs config is not available
+
+      t.flake = templates;
+
+      my_flake.flake = self;
+      m.flake = self;
+    };
+
+    gc = {
+      automatic = true;
+      dates = "weekly";
+      options = "--delete-older-than 7d";
+    };
+
+    settings = {
+      auto-optimise-store = true;
+      experimental-features = [
+        "nix-command"
+        "flakes"
+        #"ca-derivations"
+      ];
+
+      use-xdg-base-directories = true;
+
+      #substituters = ["https://cache.ngi0.nixos.org/"];
+      #trusted-public-keys = ["cache.ngi0.nixos.org-1:KqH5CBLNSyX184S9BKZJo1LxrxJ9ltnY2uAs5c/f1MA="];
+
+      fallback = true; # Build from source, if binary can't be substituted
+
+      keep-failed = true; # keep failed tmp build dirs
+      pure-eval = true; # restrict file system and network access to hash
+
+      sandbox-fallback = false; # Don't disable the sandbox, if the kernel doesn't support it
+    };
+  };
+}
diff --git a/modules/system/services/openssh/default.nix b/modules/system/services/openssh/default.nix
new file mode 100644
index 00000000..b733dbe7
--- /dev/null
+++ b/modules/system/services/openssh/default.nix
@@ -0,0 +1,15 @@
+{...}: {
+  services.openssh = {
+    enable = true;
+    hostKeys = [
+      {
+        path = "/srv/sshd/ssh_host_ed25519_key";
+        rounds = 1000;
+        type = "ed25519";
+      }
+    ];
+    settings = {
+      PasswordAuthentication = false;
+    };
+  };
+}
diff --git a/modules/system/services/postgresql/default.nix b/modules/system/services/postgresql/default.nix
new file mode 100644
index 00000000..c47a235c
--- /dev/null
+++ b/modules/system/services/postgresql/default.nix
@@ -0,0 +1,17 @@
+{
+  config,
+  lib,
+  ...
+}: let
+  cfg = config.soispha.services.postgresql;
+in {
+  options.soispha.services.postgresql = {
+    enable = lib.mkEnableOption "postgresql";
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.postgresql = {
+      enable = true;
+    };
+  };
+}
diff --git a/modules/system/services/printing/default.nix b/modules/system/services/printing/default.nix
new file mode 100644
index 00000000..85d15b16
--- /dev/null
+++ b/modules/system/services/printing/default.nix
@@ -0,0 +1,45 @@
+{
+  config,
+  lib,
+  ...
+}: let
+  cfg = config.soispha.services.printing;
+in {
+  options.soispha.services.printing = {
+    enable = lib.mkEnableOption "default printing configuration";
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.avahi = {
+      enable = true;
+      nssmdns4 = true;
+      nssmdns6 = true;
+      openFirewall = true;
+    };
+
+    services.printing = {
+      enable = true;
+      startWhenNeeded = true;
+      webInterface = true;
+
+      # deletes `/var/cache/cups`, `/var/lib/cups` and `/var/spool/cups` on cups startup
+      stateless = true;
+
+      drivers = [];
+    };
+
+    hardware = {
+      printers = {
+        ensurePrinters = [
+          {
+            name = "Brother";
+            description = "Brother DCP-9022CDW";
+            model = "everywhere";
+            deviceUri = "dnssd://Brother%20DCP-9022CDW._ipp._tcp.local/?uuid=e3248000-80ce-11db-8000-30055c773bcf";
+          }
+        ];
+        ensureDefaultPrinter = "Brother";
+      };
+    };
+  };
+}
diff --git a/modules/system/services/scanning/default.nix b/modules/system/services/scanning/default.nix
new file mode 100644
index 00000000..dda507fa
--- /dev/null
+++ b/modules/system/services/scanning/default.nix
@@ -0,0 +1,25 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}: let
+  cfg = config.soispha.services.scanning;
+in {
+  options.soispha.services.scanning = {
+    enable = lib.mkEnableOption "default scanning configuration";
+  };
+
+  config = lib.mkIf cfg.enable {
+    hardware = {
+      sane = {
+        enable = true;
+        extraBackends = [pkgs.sane-airscan];
+      };
+    };
+
+    users.users.soispha.extraGroups = [
+      "scanner" # for permission to access the scanner.
+    ];
+  };
+}
diff --git a/modules/system/services/serverphone/certificates/ca.crt b/modules/system/services/serverphone/certificates/ca.crt
new file mode 100644
index 00000000..7a4ae6f9
--- /dev/null
+++ b/modules/system/services/serverphone/certificates/ca.crt
@@ -0,0 +1,10 @@
+-----BEGIN CERTIFICATE-----

+MIIBXDCCAQOgAwIBAgIIRQ2wXiaD5pMwCgYIKoZIzj0EAwIwGTEXMBUGA1UEAwwO

+U2VydmVycGhvbmUgQ0EwHhcNMjMwNjA2MTIzNzM3WhcNMzMwNjAzMTIzNzM3WjAZ

+MRcwFQYDVQQDDA5TZXJ2ZXJwaG9uZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEH

+A0IABDZMtz3liWniBedisStXDO2sxFCKBH239ezH7uADu8g5peGssmNu1rXEDrg1

+sFwVUjQeJAocYYNoUeHiVpODf1ejNTAzMB0GA1UdDgQWBBST5oMmXrANRbCLIQpN

+W7e5uSCL3DASBgNVHRMBAf8ECDAGAQH/AgEBMAoGCCqGSM49BAMCA0cAMEQCIFig

+xA3MvRNP4uXaUEWwdP1pYL/R8N46G4NZrPEfiNV4AiA+NJSTFRCOUqEsvSb7PTFx

+YuMuJF4XxWnmStz3ym7xXA==

+-----END CERTIFICATE-----

diff --git a/modules/system/services/serverphone/certificates/server.crt b/modules/system/services/serverphone/certificates/server.crt
new file mode 100644
index 00000000..f994cdc8
--- /dev/null
+++ b/modules/system/services/serverphone/certificates/server.crt
@@ -0,0 +1,10 @@
+-----BEGIN CERTIFICATE-----

+MIIBTjCB9KADAgECAgkAhKrdjsoiOrkwCgYIKoZIzj0EAwIwGTEXMBUGA1UEAwwO

+U2VydmVycGhvbmUgQ0EwHhcNMjMwNjA2MTIzOTIwWhcNMjQwNjA1MTIzOTIwWjAm

+MSQwIgYDVQQDDBtDbGllbnQgcnVubmluZyBvbiBsb2NhbGhvc3QwWTATBgcqhkjO

+PQIBBggqhkjOPQMBBwNCAAS1ILQo8ae8ydqFlt5RncUT7joQiozk6Omunb0vxVz5

+toJRDmVqc1s6KhpCTipUV5coTcaK1TBz0+fft+9VH7cwoxgwFjAUBgNVHREEDTAL

+gglsb2NhbGhvc3QwCgYIKoZIzj0EAwIDSQAwRgIhAN7ohtsBLrjlgmSe9ngovxZM

+z61n0+/7w2mtX/OrLMWIAiEAu+D2S2o0s7E9pp2Rkug8cT5T4GCWgFgEHk5x2L/E

+RVI=

+-----END CERTIFICATE-----

diff --git a/modules/system/services/serverphone/default.nix b/modules/system/services/serverphone/default.nix
new file mode 100644
index 00000000..20125a75
--- /dev/null
+++ b/modules/system/services/serverphone/default.nix
@@ -0,0 +1,49 @@
+{
+  config,
+  serverphone,
+  system,
+  lib,
+  ...
+}: {
+  config = lib.mkIf config.soispha.secrets.enable {
+    services.serverphone = {
+      package = "${serverphone.packages.${system}.default}";
+      enable = true;
+      domain = "localhost";
+      configureDoas = true;
+      acceptedSshKeys = [
+        "AAAAC3NzaC1lZDI1NTE5AAAAIGBFuTNNn71Rhfnop2cdz3r/RhWWlCePnSBOhTBbu2ME"
+      ];
+      authorized = {
+        acceptedGpgKeys = [
+          {
+            source = ./keys/key_1;
+            trust = "ultimate";
+          }
+          {
+            source = ./keys/key_2;
+            trust = "ultimate";
+          }
+        ];
+      };
+      caCertificate = "${./certificates/ca.crt}";
+      certificate = "${./certificates/server.crt}";
+      privateKey = config.age.secrets.serverphoneServer.path;
+      certificateRequest = {
+        acceptedUsers = [
+          "soispha $argon2id$v=19$m=19456,t=2,p=1$EvhPENIBqL5b1RO5waNMWA$pJ8vDrCNJKDlqwB5bVDLjHVPEXm9McQhtt9OXSD8Zkc"
+        ];
+        caPrivateKey = config.age.secrets.serverphoneCa.path;
+      };
+    };
+
+    users.users.serverphone = {
+      group = "serverphone";
+      isSystemUser = true;
+      home = "/run/serverphone";
+    };
+    users.groups.serverphone = {
+      members = ["serverphone"];
+    };
+  };
+}
diff --git a/modules/system/services/serverphone/keys/key_1 b/modules/system/services/serverphone/keys/key_1
new file mode 120000
index 00000000..67720882
--- /dev/null
+++ b/modules/system/services/serverphone/keys/key_1
@@ -0,0 +1 @@
+../../../../home-manager/soispha/config/gpg/keys/key_1
\ No newline at end of file
diff --git a/modules/system/services/serverphone/keys/key_2 b/modules/system/services/serverphone/keys/key_2
new file mode 120000
index 00000000..24df7207
--- /dev/null
+++ b/modules/system/services/serverphone/keys/key_2
@@ -0,0 +1 @@
+../../../../home-manager/soispha/config/gpg/keys/key_2
\ No newline at end of file
diff --git a/modules/system/services/snapper/default.nix b/modules/system/services/snapper/default.nix
new file mode 100644
index 00000000..bf8201a4
--- /dev/null
+++ b/modules/system/services/snapper/default.nix
@@ -0,0 +1,53 @@
+{
+  config,
+  lib,
+  ...
+}: let
+  cfg = config.soispha.services.snapper;
+in {
+  options.soispha.services.snapper = {
+    enable = lib.mkEnableOption "snapper config";
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.snapper = {
+      configs = {
+        srv = {
+          SUBVOLUME = "/srv";
+          FSTYPE = "btrfs";
+          # users and groups allowed to work with config
+          ALLOW_GROUPS = ["wheel"];
+
+          # sync users and groups from ALLOW_USERS and ALLOW_GROUPS to .snapshots
+          # directory
+          SYNC_ACL = true;
+
+          # run daily number cleanup
+          NUMBER_CLEANUP = false;
+
+          # limit for number cleanup
+          NUMBER_MIN_AGE = 1800;
+          NUMBER_LIMIT = 50;
+          NUMBER_LIMIT_IMPORTANT = 10;
+
+          # create hourly snapshots
+          TIMELINE_CREATE = true;
+
+          # cleanup hourly snapshots after some time
+          TIMELINE_CLEANUP = true;
+
+          # limits for timeline cleanup
+          TIMELINE_MIN_AGE = 1800;
+          TIMELINE_LIMIT_HOURLY = 7;
+          TIMELINE_LIMIT_DAILY = 3;
+          TIMELINE_LIMIT_WEEKLY = 2;
+          TIMELINE_LIMIT_MONTHLY = 0;
+          TIMELINE_LIMIT_YEARLY = 2;
+
+          # cleanup empty pre-post-pairs
+          EMPTY_PRE_POST_CLEANUP = true;
+        };
+      };
+    };
+  };
+}
diff --git a/modules/system/services/steam/default.nix b/modules/system/services/steam/default.nix
new file mode 100644
index 00000000..6e507fd9
--- /dev/null
+++ b/modules/system/services/steam/default.nix
@@ -0,0 +1,23 @@
+{
+  lib,
+  config,
+  pkgs,
+  ...
+}: let
+  cfg = config.soispha.services.steam;
+in {
+  options.soispha.services.steam = {
+    enable = lib.mkEnableOption "Stream";
+  };
+
+  config = lib.mkIf cfg.enable {
+    programs.steam = {
+      enable = true;
+    };
+
+    environment.systemPackages = [
+      # TODO: Why is this package needed? <2024-05-16>
+      pkgs.wineWowPackages.waylandFull
+    ];
+  };
+}
diff --git a/modules/system/services/swaylock/default.nix b/modules/system/services/swaylock/default.nix
new file mode 100644
index 00000000..6cbcef28
--- /dev/null
+++ b/modules/system/services/swaylock/default.nix
@@ -0,0 +1,4 @@
+{...}: {
+  # otherwise swaylock can't access the user password.
+  security.pam.services.swaylock = {};
+}
diff --git a/modules/system/services/xdg/default.nix b/modules/system/services/xdg/default.nix
new file mode 100644
index 00000000..5140a832
--- /dev/null
+++ b/modules/system/services/xdg/default.nix
@@ -0,0 +1,58 @@
+{
+  pkgs,
+  nixpkgs_open_prs,
+  sysLib,
+  system,
+  ...
+}: let
+  pkgs_tfc = nixpkgs_open_prs.nixpkgs-tfc.legacyPackages."${system}";
+in {
+  services.dbus.enable = true;
+  xdg = {
+    portal = {
+      enable = true;
+      termfilechooser = {
+        enable = true;
+        logLevel = "TRACE";
+        package = pkgs_tfc.xdg-desktop-portal-termfilechooser;
+        settings = {
+          filechooser = {
+            default_dir = "/tmp";
+            cmd = "${sysLib.writeShellScript {
+              src = ./scripts/lf_wrapper.sh;
+              name = "lf_wrapper";
+              keepPath = true;
+              dependencies = with pkgs; [
+                lf
+                alacritty
+                bash
+              ];
+            }}/bin/lf_wrapper";
+          };
+        };
+      };
+      wlr = {
+        enable = true;
+      };
+      config = {
+        common = {
+          # NOTE: The next entry is supposedly needed for gtk based apps <2023-08-31>
+          default = ["wlr" "gtk"];
+          "org.freedesktop.impl.portal.FileChooser" = ["termfilechooser"];
+        };
+
+        # TODO: Also activate, when on another wlr-based compositor <2023-11-25>
+        river = {
+          default = ["wlr" "gtk"];
+          "org.freedesktop.impl.portal.FileChooser" = ["termfilechooser"];
+        };
+      };
+      extraPortals = [
+        pkgs.xdg-desktop-portal-gtk
+        pkgs.xdg-desktop-portal-wlr
+        pkgs_tfc.xdg-desktop-portal-termfilechooser
+      ];
+    };
+  };
+  # TODO: mime = {};
+}
diff --git a/modules/system/services/xdg/scripts/lf_wrapper.sh b/modules/system/services/xdg/scripts/lf_wrapper.sh
new file mode 100755
index 00000000..16603fe4
--- /dev/null
+++ b/modules/system/services/xdg/scripts/lf_wrapper.sh
@@ -0,0 +1,79 @@
+#!/usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+
+# This wrapper script is invoked by xdg-desktop-portal-termfilechooser.
+#
+# Inputs:
+# 1. "1" if multiple files can be chosen, "0" otherwise.
+# 2. "1" if a directory should be chosen, "0" otherwise.
+# 3. "0" if opening files was requested, "1" if writing to a file was
+#    requested. For example, when uploading files in Firefox, this will be "0".
+#    When saving a web page in Firefox, this will be "1".
+# 4. If writing to a file, this is recommended path provided by the caller. For
+#    example, when saving a web page in Firefox, this will be the recommended
+#    path Firefox provided, such as "~/Downloads/webpage_title.html".
+#    Note that if the path already exists, we keep appending "_" to it until we
+#    get a path that does not exist.
+# 5. The output path, to which results should be written.
+#
+# Output:
+# The script should print the selected paths to the output path (argument #5),
+# one path per line.
+# If nothing is printed, then the operation is assumed to have been canceled.
+
+multiple="$1"
+directory="$2"
+save="$3"
+recommended_path="$4"
+out="$5"
+
+# echo > /tmp/stdout
+# echo > /tmp/stderr
+#
+# exec 1>> /tmp/stdout
+# exec 2>> /tmp/stderr
+
+cmd="$(command -v lf)"
+termcmd="${TERMINAL:-$(command -v alacritty)}"
+
+if [ "$save" = "1" ]; then
+    set -- -selection-path="$out" -command='set promptfmt "Select the file to write to %S \033[32;1m%u@%h\033[0m:\033[34;1m%d\033[0m\033[1m%f\033[0m"' "$recommended_path"
+    cat <<EOF >"$recommended_path"
+xdg-desktop-portal-termfilechooser saving files tutorial
+
+!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+!!!                 === WARNING! ===                 !!!
+!!! The contents of *whatever* file you open last in !!!
+!!!            lf will be *overwritten*!             !!!
+!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+Instructions:
+1) Move this file wherever you want.
+2) Rename the file if needed.
+3) Confirm your selection by opening the file, for
+   example by pressing <Enter>.
+
+Notes:
+1) This file is provided for your convenience. You
+   could delete it and choose another file to overwrite
+   that, for example.
+2) If you quit lf without opening a file, this file
+   will be removed and the save operation aborted.
+EOF
+
+elif [ "$directory" = "1" ]; then
+    set -- -selection-path="$out" -command='set dironly' -command='set promptfmt "Select directory (quit in dir to select it) %S \033[32;1m%u@%h\033[0m:\033[34;1m%d\033[0m\033[1m%f\033[0m"'
+elif [ "$multiple" = "1" ]; then
+    set -- -selection-path="$out" -command='set promptfmt "Select file(s) (open file to select it; <Space> to select multiple) %S \033[32;1m%u@%h\033[0m:\033[34;1m%d\033[0m\033[1m%f\033[0m"'
+else
+    set -- -selection-path="$out" -command='set promptfmt "Select file (open file to select it) %S \033[32;1m%u@%h\033[0m:\033[34;1m%d\033[0m\033[1m%f\033[0m"'
+fi
+
+"$termcmd" --title 'floating please' -e "$cmd" "$@"
+
+if [ "$save" = "1" ] && [ ! -s "$out" ]; then
+    rm "$recommended_path"
+fi
+# vim: ft=sh
diff --git a/modules/system/services/xdg/scripts/ranger_wrapper.sh b/modules/system/services/xdg/scripts/ranger_wrapper.sh
new file mode 100755
index 00000000..e148bf19
--- /dev/null
+++ b/modules/system/services/xdg/scripts/ranger_wrapper.sh
@@ -0,0 +1,68 @@
+#!/usr/bin/env dash
+
+# shellcheck source=/dev/null
+SHELL_LIBRARY_VERSION="2.1.2" . %SHELL_LIBRARY_PATH
+# This wrapper script is invoked by xdg-desktop-portal-termfilechooser.
+#
+# Inputs:
+# 1. "1" if multiple files can be chosen, "0" otherwise.
+# 2. "1" if a directory should be chosen, "0" otherwise.
+# 3. "0" if opening files was requested, "1" if writing to a file was
+#    requested. For example, when uploading files in Firefox, this will be "0".
+#    When saving a web page in Firefox, this will be "1".
+# 4. If writing to a file, this is recommended path provided by the caller. For
+#    example, when saving a web page in Firefox, this will be the recommended
+#    path Firefox provided, such as "~/Downloads/webpage_title.html".
+#    Note that if the path already exists, we keep appending "_" to it until we
+#    get a path that does not exist.
+# 5. The output path, to which results should be written.
+#
+# Output:
+# The script should print the selected paths to the output path (argument #5),
+# one path per line.
+# If nothing is printed, then the operation is assumed to have been canceled.
+
+multiple="$1"
+directory="$2"
+save="$3"
+path="$4"
+out="$5"
+
+cmd="$(command -v ranger)"
+termcmd="${TERMCMD:-$(command -v kitty)}"
+
+if [ "$save" = "1" ]; then
+    set -- --choosefile="$out" --cmd='echo Select save path (see tutorial in preview pane; try pressing zv or zp if no preview)' "$path"
+    printf '%s' 'xdg-desktop-portal-termfilechooser saving files tutorial
+
+!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+!!!                 === WARNING! ===                 !!!
+!!! The contents of *whatever* file you open last in !!!
+!!! ranger will be *overwritten*!                    !!!
+!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+Instructions:
+1) Move this file wherever you want.
+2) Rename the file if needed.
+3) Confirm your selection by opening the file, for
+   example by pressing <Enter>.
+
+Notes:
+1) This file is provided for your convenience. You
+   could delete it and choose another file to overwrite
+   that, for example.
+2) If you quit ranger without opening a file, this file
+   will be removed and the save operation aborted.
+' >"$path"
+elif [ "$directory" = "1" ]; then
+    set -- --choosedir="$out" --show-only-dirs --cmd="echo Select directory (quit in dir to select it)"
+elif [ "$multiple" = "1" ]; then
+    set -- --choosefiles="$out" --cmd="echo Select file(s) (open file to select it; <Space> to select multiple)"
+else
+    set -- --choosefile="$out" --cmd="echo Select file (open file to select it)"
+fi
+
+"$termcmd" -- "$cmd" "$@"
+if [ "$save" = "1" ] && [ ! -s "$out" ]; then
+    rm "$path"
+fi