{
  config,
  lib,
  pkgs,
  ...
}:
with lib; let
  cfg = config.custom.services.systemd-lock-handler;

  gtklock = getExe pkgs.gtklock;
  hyprlock = getExe config.programs.hyprlock.package;
  niri = getExe config.programs.niri.package;
  pgrep = getExe' pkgs.procps "pgrep";
  sleep = getExe' pkgs.coreutils "sleep";
in {
  options.custom.services.systemd-lock-handler = {
    enable = mkOption {default = false;};
    delay = mkOption {default = 1;}; # Seconds

    lock = mkOption {
      default =
        if config.custom.lockscreen == "gtklock"
        then gtklock
        else if config.custom.lockscreen == "hyprlock"
        then hyprlock
        else "";
    };

    transition = mkOption {default = true;};
  };

  config = mkIf cfg.enable {
    # https://sr.ht/~whynothugo/systemd-lock-handler/
    services.systemd-lock-handler.enable = true;

    # https://sr.ht/~whynothugo/systemd-lock-handler/#usage
    # https://github.com/hyprwm/hypridle/issues/49
    systemd.user.services = {
      handle-lock = let
        delay-ms = toString (cfg.delay * 1000); # Milliseconds
      in {
        unitConfig = {
          Description = "Lockscreen";

          # Use Before, not After lock.target
          # https://todo.sr.ht/~whynothugo/systemd-lock-handler/4
          Before = ["lock.target"];
        };

        serviceConfig = {
          Type = "exec";

          ExecCondition = pkgs.writeShellScript "lock-condition" ''
            if ${pgrep} ${config.custom.lockscreen}; then
              exit 1 # Lockscreen process already running
            else
              exit 0 # Lockscreen process not found
            fi
          '';

          # HACK: Default red background immediately shows while lockscreen starts, so use transition
          # https://github.com/YaLTeR/niri/issues/808
          ExecStartPre = mkIf cfg.transition (
            if config.custom.desktop == "niri"
            then "${niri} msg action do-screen-transition --delay-ms ${delay-ms}"
            else ""
          );

          ExecStart = cfg.lock;
        };

        requiredBy = ["lock.target" "sleep.target"];
      };

      handle-sleep = let
        # Transition time is about 1 second
        delay = toString (cfg.delay + 1); # Seconds
      in {
        unitConfig = {
          Description = "Delay sleep for ${delay}s";
          Before = ["sleep.target"];
        };

        serviceConfig = {
          Type = "oneshot";
          ExecStart = "${sleep} ${delay}s";
        };

        requiredBy = ["sleep.target"];
      };
    };
  };
}