{ config, pkgs, lib, ... }: let inherit (lib) mkOption mkForce getExe getExe' listToAttrs flatten mapAttrsToList mapAttrs mapAttrs' nameValuePair toLower replaceStrings concatMapStringsSep ; partOf = cfg: "${replaceStrings [" "] ["-"] (toLower cfg.name)}.target"; # make a firefox webapp + hidden .desktop entry for the client app make-firefox = cfg: mapAttrs' ( name: cfg: nameValuePair "${name}-client" { inherit (cfg) name; url = "http://127.0.0.1:${builtins.toString cfg.port}"; extraSettings = config.programs.firefox.profiles.default.settings; hidden = true; } ) cfg; # make a systemd service for running the frontend make-systemd-service = cfg: mapAttrs' ( name: cfg: if (cfg.service != null) then nameValuePair "${cfg.name}-frontend" { Unit = { Description = "${cfg.name} Frontend"; WantedBy = mkForce []; }; Service = cfg.service; } else nameValuePair "" {} ) cfg; # modify systemd units to be PartOf this target modify-systemd-services = cfg: listToAttrs (flatten (mapAttrsToList ( name: cfg: (map ( req: { name = "${req}"; value = { Unit = { PartOf = partOf cfg; }; }; } ) cfg.requires.services) ) cfg)); modify-quadlets = cfg: listToAttrs (flatten (mapAttrsToList ( name: cfg: (map ( req: { name = "${req}"; value = { unitConfig = { PartOf = partOf cfg; }; }; } ) cfg.requires.containers) ) cfg)); # make a systemd target to collate dependencies make-systemd-target = cfg: mapAttrs ( name: cfg: { Unit = { Description = "${cfg.name} Target"; WantedBy = mkForce []; Requires = (map (req: req + ".service") cfg.requires.services) ++ (map (req: "podman-" + req + ".service") cfg.requires.containers); }; } ) cfg; # make desktop shortcuts and a script which will handle starting everything make-xdg = cfg: mapAttrs ( name: cfg: { inherit (cfg) name icon genericName; type = "Application"; exec = "${let notify-send = "${getExe' pkgs.libnotify "notify-send"} -a \"${cfg.name}\""; systemctl = "${getExe' pkgs.systemd "systemctl"}"; dex = "${getExe pkgs.dex}"; podman = "${getExe pkgs.podman}"; makeContainerCheck = container: ''[ "$(${podman} inspect -f {{.State.Health.Status}} ${container})" == "healthy" ]''; # makeContainerCheck = container: '' # [ ${podman} inspect -f {{.State.Status}} ${container})" != "running" ] # ''; containerChecks = if (cfg.requires.containers != []) then '' container_checks() { if '' + (concatMapStringsSep " && " (container: makeContainerCheck container) cfg.requires.containers) + '' ; then return 0 else return 1 fi } '' else '' container_checks() { return 0 } ''; in pkgs.writeShellScript "${name}" '' set -euo pipefail exit_error() { ${notify-send} -w "Failure" $1 exit 1 } ${containerChecks} ${notify-send} "Launching ${name} backend.." "Please be patient." ${systemctl} --user start ${name}.target || exit_error "Failed to launch!" checks=0 until container_checks; do sleep 2 checks=$((checks+1)) if [ $((checks%10)) -eq 0 ]; then ${notify-send} "Waiting for backend." fi if [ $checks -ge 60 ]; then ${systemctl} --no-block --user stop ${name}.target exit_error "Failed to launch!" fi done ${notify-send} "Launching ${name}.." ${dex} -w ~/.nix-profile/share/applications/${name}-client.desktop ${notify-send} "Goodbye" "Shutting down." ${systemctl} --user stop ${name}.target exit 0 ''}"; } ) cfg; cfg = config.localWebApps; in { options.localWebApps = mkOption { default = {}; type = with lib.types; attrsOf (submodule { options = { name = mkOption { type = str; description = "Display name of the webapp."; }; genericName = mkOption { type = nullOr str; description = "Generic name of the webapp."; default = null; }; icon = mkOption { type = nullOr (either str path); description = "Path to a file to use for application icon."; default = null; }; requires = mkOption { type = nullOr (submodule { options = { containers = mkOption { type = listOf str; default = []; }; services = mkOption { type = listOf str; default = []; }; }; }); default = null; description = "Containers or services this app requires."; }; service = mkOption { type = nullOr (submodule { options = { execStartPre = mkOption { type = nullOr str; default = null; }; execStart = mkOption { type = nullOr str; default = null; }; execStop = mkOption { type = nullOr str; default = null; }; }; }); default = null; description = "Submodule containing exec[StartPre/Start/Stop] commands for any required systemd service"; }; port = mkOption { type = int; description = "Local port the webapp should host on."; }; }; }); }; config = { programs.firefox.webapps = make-firefox cfg; systemd.user.targets = make-systemd-target cfg; systemd.user.services = (make-systemd-service cfg) // (modify-systemd-services cfg); services.podman.containers = modify-quadlets cfg; xdg.desktopEntries = make-xdg cfg; }; }