summary refs log tree commit diff
path: root/nixos/modules/services/misc/autorandr.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services/misc/autorandr.nix')
-rw-r--r--nixos/modules/services/misc/autorandr.nix310
1 files changed, 308 insertions, 2 deletions
diff --git a/nixos/modules/services/misc/autorandr.nix b/nixos/modules/services/misc/autorandr.nix
index a65c5c9d11c..ef799e9ce3b 100644
--- a/nixos/modules/services/misc/autorandr.nix
+++ b/nixos/modules/services/misc/autorandr.nix
@@ -5,6 +5,243 @@ with lib;
 let
 
   cfg = config.services.autorandr;
+  hookType = types.lines;
+
+  matrixOf = n: m: elemType:
+  mkOptionType rec {
+    name = "matrixOf";
+    description =
+      "${toString n}×${toString m} matrix of ${elemType.description}s";
+    check = xss:
+      let listOfSize = l: xs: isList xs && length xs == l;
+      in listOfSize n xss
+      && all (xs: listOfSize m xs && all elemType.check xs) xss;
+    merge = mergeOneOption;
+    getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "*" "*" ]);
+    getSubModules = elemType.getSubModules;
+    substSubModules = mod: matrixOf n m (elemType.substSubModules mod);
+    functor = (defaultFunctor name) // { wrapped = elemType; };
+  };
+
+  profileModule = types.submodule {
+    options = {
+      fingerprint = mkOption {
+        type = types.attrsOf types.str;
+        description = ''
+          Output name to EDID mapping.
+          Use <code>autorandr --fingerprint</code> to get current setup values.
+        '';
+        default = { };
+      };
+
+      config = mkOption {
+        type = types.attrsOf configModule;
+        description = "Per output profile configuration.";
+        default = { };
+      };
+
+      hooks = mkOption {
+        type = hooksModule;
+        description = "Profile hook scripts.";
+        default = { };
+      };
+    };
+  };
+
+  configModule = types.submodule {
+    options = {
+      enable = mkOption {
+        type = types.bool;
+        description = "Whether to enable the output.";
+        default = true;
+      };
+
+      crtc = mkOption {
+        type = types.nullOr types.ints.unsigned;
+        description = "Output video display controller.";
+        default = null;
+        example = 0;
+      };
+
+      primary = mkOption {
+        type = types.bool;
+        description = "Whether output should be marked as primary";
+        default = false;
+      };
+
+      position = mkOption {
+        type = types.str;
+        description = "Output position";
+        default = "";
+        example = "5760x0";
+      };
+
+      mode = mkOption {
+        type = types.str;
+        description = "Output resolution.";
+        default = "";
+        example = "3840x2160";
+      };
+
+      rate = mkOption {
+        type = types.str;
+        description = "Output framerate.";
+        default = "";
+        example = "60.00";
+      };
+
+      gamma = mkOption {
+        type = types.str;
+        description = "Output gamma configuration.";
+        default = "";
+        example = "1.0:0.909:0.833";
+      };
+
+      rotate = mkOption {
+        type = types.nullOr (types.enum [ "normal" "left" "right" "inverted" ]);
+        description = "Output rotate configuration.";
+        default = null;
+        example = "left";
+      };
+
+      transform = mkOption {
+        type = types.nullOr (matrixOf 3 3 types.float);
+        default = null;
+        example = literalExpression ''
+          [
+            [ 0.6 0.0 0.0 ]
+            [ 0.0 0.6 0.0 ]
+            [ 0.0 0.0 1.0 ]
+          ]
+        '';
+        description = ''
+          Refer to
+          <citerefentry>
+            <refentrytitle>xrandr</refentrytitle>
+            <manvolnum>1</manvolnum>
+          </citerefentry>
+          for the documentation of the transform matrix.
+        '';
+      };
+
+      dpi = mkOption {
+        type = types.nullOr types.ints.positive;
+        description = "Output DPI configuration.";
+        default = null;
+        example = 96;
+      };
+
+      scale = mkOption {
+        type = types.nullOr (types.submodule {
+          options = {
+            method = mkOption {
+              type = types.enum [ "factor" "pixel" ];
+              description = "Output scaling method.";
+              default = "factor";
+              example = "pixel";
+            };
+
+            x = mkOption {
+              type = types.either types.float types.ints.positive;
+              description = "Horizontal scaling factor/pixels.";
+            };
+
+            y = mkOption {
+              type = types.either types.float types.ints.positive;
+              description = "Vertical scaling factor/pixels.";
+            };
+          };
+        });
+        description = ''
+          Output scale configuration.
+          </para><para>
+          Either configure by pixels or a scaling factor. When using pixel method the
+          <citerefentry>
+            <refentrytitle>xrandr</refentrytitle>
+            <manvolnum>1</manvolnum>
+          </citerefentry>
+          option
+          <parameter class="command">--scale-from</parameter>
+          will be used; when using factor method the option
+          <parameter class="command">--scale</parameter>
+          will be used.
+          </para><para>
+          This option is a shortcut version of the transform option and they are mutually
+          exclusive.
+        '';
+        default = null;
+        example = literalExpression ''
+          {
+            x = 1.25;
+            y = 1.25;
+          }
+        '';
+      };
+    };
+  };
+
+  hooksModule = types.submodule {
+    options = {
+      postswitch = mkOption {
+        type = types.attrsOf hookType;
+        description = "Postswitch hook executed after mode switch.";
+        default = { };
+      };
+
+      preswitch = mkOption {
+        type = types.attrsOf hookType;
+        description = "Preswitch hook executed before mode switch.";
+        default = { };
+      };
+
+      predetect = mkOption {
+        type = types.attrsOf hookType;
+        description = ''
+          Predetect hook executed before autorandr attempts to run xrandr.
+        '';
+        default = { };
+      };
+    };
+  };
+
+  hookToFile = folder: name: hook:
+    nameValuePair "xdg/autorandr/${folder}/${name}" {
+      source = "${pkgs.writeShellScriptBin "hook" hook}/bin/hook";
+    };
+  profileToFiles = name: profile:
+    with profile;
+    mkMerge ([
+      {
+        "xdg/autorandr/${name}/setup".text = concatStringsSep "\n"
+          (mapAttrsToList fingerprintToString fingerprint);
+        "xdg/autorandr/${name}/config".text =
+          concatStringsSep "\n" (mapAttrsToList configToString profile.config);
+      }
+      (mapAttrs' (hookToFile "${name}/postswitch.d") hooks.postswitch)
+      (mapAttrs' (hookToFile "${name}/preswitch.d") hooks.preswitch)
+      (mapAttrs' (hookToFile "${name}/predetect.d") hooks.predetect)
+    ]);
+  fingerprintToString = name: edid: "${name} ${edid}";
+  configToString = name: config:
+    if config.enable then
+      concatStringsSep "\n" ([ "output ${name}" ]
+        ++ optional (config.position != "") "pos ${config.position}"
+        ++ optional (config.crtc != null) "crtc ${toString config.crtc}"
+        ++ optional config.primary "primary"
+        ++ optional (config.dpi != null) "dpi ${toString config.dpi}"
+        ++ optional (config.gamma != "") "gamma ${config.gamma}"
+        ++ optional (config.mode != "") "mode ${config.mode}"
+        ++ optional (config.rate != "") "rate ${config.rate}"
+        ++ optional (config.rotate != null) "rotate ${config.rotate}"
+        ++ optional (config.transform != null) ("transform "
+          + concatMapStringsSep "," toString (flatten config.transform))
+        ++ optional (config.scale != null)
+        ((if config.scale.method == "factor" then "scale" else "scale-from")
+          + " ${toString config.scale.x}x${toString config.scale.y}"))
+    else ''
+      output ${name}
+      off
+    '';
 
 in {
 
@@ -22,6 +259,67 @@ in {
           for further reference.
         '';
       };
+
+      hooks = mkOption {
+        type = hooksModule;
+        description = "Global hook scripts";
+        default = { };
+        example = ''
+          {
+            postswitch = {
+              "notify-i3" = "''${pkgs.i3}/bin/i3-msg restart";
+              "change-background" = readFile ./change-background.sh;
+              "change-dpi" = '''
+                case "$AUTORANDR_CURRENT_PROFILE" in
+                  default)
+                    DPI=120
+                    ;;
+                  home)
+                    DPI=192
+                    ;;
+                  work)
+                    DPI=144
+                    ;;
+                  *)
+                    echo "Unknown profle: $AUTORANDR_CURRENT_PROFILE"
+                    exit 1
+                esac
+                echo "Xft.dpi: $DPI" | ''${pkgs.xorg.xrdb}/bin/xrdb -merge
+              '''
+            };
+          }
+        '';
+      };
+      profiles = mkOption {
+        type = types.attrsOf profileModule;
+        description = "Autorandr profiles specification.";
+        default = { };
+        example = literalExpression ''
+          {
+            "work" = {
+              fingerprint = {
+                eDP1 = "<EDID>";
+                DP1 = "<EDID>";
+              };
+              config = {
+                eDP1.enable = false;
+                DP1 = {
+                  enable = true;
+                  crtc = 0;
+                  primary = true;
+                  position = "0x0";
+                  mode = "3840x2160";
+                  gamma = "1.0:0.909:0.833";
+                  rate = "60.00";
+                  rotate = "left";
+                };
+              };
+              hooks.postswitch = readFile ./work-postswitch.sh;
+            };
+          }
+        '';
+      };
+
     };
 
   };
@@ -30,7 +328,15 @@ in {
 
     services.udev.packages = [ pkgs.autorandr ];
 
-    environment.systemPackages = [ pkgs.autorandr ];
+    environment = {
+      systemPackages = [ pkgs.autorandr ];
+      etc = mkMerge ([
+        (mapAttrs' (hookToFile "postswitch.d") cfg.hooks.postswitch)
+        (mapAttrs' (hookToFile "preswitch.d") cfg.hooks.preswitch)
+        (mapAttrs' (hookToFile "predetect.d") cfg.hooks.predetect)
+        (mkMerge (mapAttrsToList profileToFiles cfg.profiles))
+      ]);
+    };
 
     systemd.services.autorandr = {
       wantedBy = [ "sleep.target" ];
@@ -49,5 +355,5 @@ in {
 
   };
 
-  meta.maintainers = with maintainers; [ ];
+  meta.maintainers = with maintainers; [ alexnortung ];
 }