1 { config, lib, pkgs, ... }:
7 cfg = config.services.autorandr;
8 hookType = types.lines;
10 matrixOf = n: m: elemType:
14 "${toString n}×${toString m} matrix of ${elemType.description}s";
16 let listOfSize = l: xs: isList xs && length xs == l;
18 && all (xs: listOfSize m xs && all elemType.check xs) xss;
19 merge = mergeOneOption;
20 getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "*" "*" ]);
21 getSubModules = elemType.getSubModules;
22 substSubModules = mod: matrixOf n m (elemType.substSubModules mod);
23 functor = (defaultFunctor name) // { wrapped = elemType; };
26 profileModule = types.submodule {
28 fingerprint = mkOption {
29 type = types.attrsOf types.str;
30 description = lib.mdDoc ''
31 Output name to EDID mapping.
32 Use `autorandr --fingerprint` to get current setup values.
38 type = types.attrsOf configModule;
39 description = lib.mdDoc "Per output profile configuration.";
45 description = lib.mdDoc "Profile hook scripts.";
51 configModule = types.submodule {
55 description = lib.mdDoc "Whether to enable the output.";
60 type = types.nullOr types.ints.unsigned;
61 description = lib.mdDoc "Output video display controller.";
68 description = lib.mdDoc "Whether output should be marked as primary";
74 description = lib.mdDoc "Output position";
81 description = lib.mdDoc "Output resolution.";
83 example = "3840x2160";
88 description = lib.mdDoc "Output framerate.";
95 description = lib.mdDoc "Output gamma configuration.";
97 example = "1.0:0.909:0.833";
101 type = types.nullOr (types.enum [ "normal" "left" "right" "inverted" ]);
102 description = lib.mdDoc "Output rotate configuration.";
107 transform = mkOption {
108 type = types.nullOr (matrixOf 3 3 types.float);
110 example = literalExpression ''
117 description = lib.mdDoc ''
120 for the documentation of the transform matrix.
125 type = types.nullOr types.ints.positive;
126 description = lib.mdDoc "Output DPI configuration.";
132 type = types.nullOr (types.submodule {
135 type = types.enum [ "factor" "pixel" ];
136 description = lib.mdDoc "Output scaling method.";
142 type = types.either types.float types.ints.positive;
143 description = lib.mdDoc "Horizontal scaling factor/pixels.";
147 type = types.either types.float types.ints.positive;
148 description = lib.mdDoc "Vertical scaling factor/pixels.";
152 description = lib.mdDoc ''
153 Output scale configuration.
155 Either configure by pixels or a scaling factor. When using pixel method the
159 will be used; when using factor method the option
163 This option is a shortcut version of the transform option and they are mutually
167 example = literalExpression ''
177 hooksModule = types.submodule {
179 postswitch = mkOption {
180 type = types.attrsOf hookType;
181 description = lib.mdDoc "Postswitch hook executed after mode switch.";
185 preswitch = mkOption {
186 type = types.attrsOf hookType;
187 description = lib.mdDoc "Preswitch hook executed before mode switch.";
191 predetect = mkOption {
192 type = types.attrsOf hookType;
193 description = lib.mdDoc ''
194 Predetect hook executed before autorandr attempts to run xrandr.
201 hookToFile = folder: name: hook:
202 nameValuePair "xdg/autorandr/${folder}/${name}" {
203 source = "${pkgs.writeShellScriptBin "hook" hook}/bin/hook";
205 profileToFiles = name: profile:
209 "xdg/autorandr/${name}/setup".text = concatStringsSep "\n"
210 (mapAttrsToList fingerprintToString fingerprint);
211 "xdg/autorandr/${name}/config".text =
212 concatStringsSep "\n" (mapAttrsToList configToString profile.config);
214 (mapAttrs' (hookToFile "${name}/postswitch.d") hooks.postswitch)
215 (mapAttrs' (hookToFile "${name}/preswitch.d") hooks.preswitch)
216 (mapAttrs' (hookToFile "${name}/predetect.d") hooks.predetect)
218 fingerprintToString = name: edid: "${name} ${edid}";
219 configToString = name: config:
220 if config.enable then
221 concatStringsSep "\n" ([ "output ${name}" ]
222 ++ optional (config.position != "") "pos ${config.position}"
223 ++ optional (config.crtc != null) "crtc ${toString config.crtc}"
224 ++ optional config.primary "primary"
225 ++ optional (config.dpi != null) "dpi ${toString config.dpi}"
226 ++ optional (config.gamma != "") "gamma ${config.gamma}"
227 ++ optional (config.mode != "") "mode ${config.mode}"
228 ++ optional (config.rate != "") "rate ${config.rate}"
229 ++ optional (config.rotate != null) "rotate ${config.rotate}"
230 ++ optional (config.transform != null) ("transform "
231 + concatMapStringsSep "," toString (flatten config.transform))
232 ++ optional (config.scale != null)
233 ((if config.scale.method == "factor" then "scale" else "scale-from")
234 + " ${toString config.scale.x}x${toString config.scale.y}"))
244 services.autorandr = {
245 enable = mkEnableOption (lib.mdDoc "handling of hotplug and sleep events by autorandr");
247 defaultTarget = mkOption {
250 description = lib.mdDoc ''
251 Fallback if no monitor layout can be detected. See the docs
252 (https://github.com/phillipberndt/autorandr/blob/v1.0/README.md#how-to-use)
253 for further reference.
259 description = lib.mdDoc "Global hook scripts";
264 "notify-i3" = "''${pkgs.i3}/bin/i3-msg restart";
265 "change-background" = readFile ./change-background.sh;
267 case "$AUTORANDR_CURRENT_PROFILE" in
278 echo "Unknown profle: $AUTORANDR_CURRENT_PROFILE"
281 echo "Xft.dpi: $DPI" | ''${pkgs.xorg.xrdb}/bin/xrdb -merge
287 profiles = mkOption {
288 type = types.attrsOf profileModule;
289 description = lib.mdDoc "Autorandr profiles specification.";
291 example = literalExpression ''
306 gamma = "1.0:0.909:0.833";
311 hooks.postswitch = readFile ./work-postswitch.sh;
321 config = mkIf cfg.enable {
323 services.udev.packages = [ pkgs.autorandr ];
326 systemPackages = [ pkgs.autorandr ];
328 (mapAttrs' (hookToFile "postswitch.d") cfg.hooks.postswitch)
329 (mapAttrs' (hookToFile "preswitch.d") cfg.hooks.preswitch)
330 (mapAttrs' (hookToFile "predetect.d") cfg.hooks.predetect)
331 (mkMerge (mapAttrsToList profileToFiles cfg.profiles))
335 systemd.services.autorandr = {
336 wantedBy = [ "sleep.target" ];
337 description = "Autorandr execution hook";
338 after = [ "sleep.target" ];
340 startLimitIntervalSec = 5;
343 ExecStart = "${pkgs.autorandr}/bin/autorandr --batch --change --default ${cfg.defaultTarget}";
345 RemainAfterExit = false;
346 KillMode = "process";
352 meta.maintainers = with maintainers; [ alexnortung ];