1 { config, lib, pkgs, ... }:
4 cfg = config.services.autorandr;
5 hookType = lib.types.lines;
7 matrixOf = n: m: elemType:
11 "${toString n}×${toString m} matrix of ${elemType.description}s";
13 let listOfSize = l: xs: lib.isList xs && lib.length xs == l;
15 && lib.all (xs: listOfSize m xs && lib.all elemType.check xs) xss;
16 merge = lib.mergeOneOption;
17 getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "*" "*" ]);
18 getSubModules = elemType.getSubModules;
19 substSubModules = mod: matrixOf n m (elemType.substSubModules mod);
20 functor = (lib.defaultFunctor name) // { wrapped = elemType; };
23 profileModule = lib.types.submodule {
25 fingerprint = lib.mkOption {
26 type = lib.types.attrsOf lib.types.str;
28 Output name to EDID mapping.
29 Use `autorandr --fingerprint` to get current setup values.
34 config = lib.mkOption {
35 type = lib.types.attrsOf configModule;
36 description = "Per output profile configuration.";
40 hooks = lib.mkOption {
42 description = "Profile hook scripts.";
48 configModule = lib.types.submodule {
50 enable = lib.mkOption {
51 type = lib.types.bool;
52 description = "Whether to enable the output.";
57 type = lib.types.nullOr lib.types.ints.unsigned;
58 description = "Output video display controller.";
63 primary = lib.mkOption {
64 type = lib.types.bool;
65 description = "Whether output should be marked as primary";
69 position = lib.mkOption {
71 description = "Output position";
78 description = "Output resolution.";
80 example = "3840x2160";
85 description = "Output framerate.";
90 gamma = lib.mkOption {
92 description = "Output gamma configuration.";
94 example = "1.0:0.909:0.833";
97 rotate = lib.mkOption {
98 type = lib.types.nullOr (lib.types.enum [ "normal" "left" "right" "inverted" ]);
99 description = "Output rotate configuration.";
104 transform = lib.mkOption {
105 type = lib.types.nullOr (matrixOf 3 3 lib.types.float);
107 example = lib.literalExpression ''
117 for the documentation of the transform matrix.
122 type = lib.types.nullOr lib.types.ints.positive;
123 description = "Output DPI configuration.";
128 scale = lib.mkOption {
129 type = lib.types.nullOr (lib.types.submodule {
131 method = lib.mkOption {
132 type = lib.types.enum [ "factor" "pixel" ];
133 description = "Output scaling method.";
139 type = lib.types.either lib.types.float lib.types.ints.positive;
140 description = "Horizontal scaling factor/pixels.";
144 type = lib.types.either lib.types.float lib.types.ints.positive;
145 description = "Vertical scaling factor/pixels.";
150 Output scale configuration.
152 Either configure by pixels or a scaling factor. When using pixel method the
156 will be used; when using factor method the option
160 This option is a shortcut version of the transform option and they are mutually
164 example = lib.literalExpression ''
174 hooksModule = lib.types.submodule {
176 postswitch = lib.mkOption {
177 type = lib.types.attrsOf hookType;
178 description = "Postswitch hook executed after mode switch.";
182 preswitch = lib.mkOption {
183 type = lib.types.attrsOf hookType;
184 description = "Preswitch hook executed before mode switch.";
188 predetect = lib.mkOption {
189 type = lib.types.attrsOf hookType;
191 Predetect hook executed before autorandr attempts to run xrandr.
198 hookToFile = folder: name: hook:
199 lib.nameValuePair "xdg/autorandr/${folder}/${name}" {
200 source = "${pkgs.writeShellScriptBin "hook" hook}/bin/hook";
202 profileToFiles = name: profile:
206 "xdg/autorandr/${name}/setup".text = lib.concatStringsSep "\n"
207 (lib.mapAttrsToList fingerprintToString fingerprint);
208 "xdg/autorandr/${name}/config".text =
209 lib.concatStringsSep "\n" (lib.mapAttrsToList configToString profile.config);
211 (lib.mapAttrs' (hookToFile "${name}/postswitch.d") hooks.postswitch)
212 (lib.mapAttrs' (hookToFile "${name}/preswitch.d") hooks.preswitch)
213 (lib.mapAttrs' (hookToFile "${name}/predetect.d") hooks.predetect)
215 fingerprintToString = name: edid: "${name} ${edid}";
216 configToString = name: config:
217 if config.enable then
218 lib.concatStringsSep "\n" ([ "output ${name}" ]
219 ++ lib.optional (config.position != "") "pos ${config.position}"
220 ++ lib.optional (config.crtc != null) "crtc ${toString config.crtc}"
221 ++ lib.optional config.primary "primary"
222 ++ lib.optional (config.dpi != null) "dpi ${toString config.dpi}"
223 ++ lib.optional (config.gamma != "") "gamma ${config.gamma}"
224 ++ lib.optional (config.mode != "") "mode ${config.mode}"
225 ++ lib.optional (config.rate != "") "rate ${config.rate}"
226 ++ lib.optional (config.rotate != null) "rotate ${config.rotate}"
227 ++ lib.optional (config.transform != null) ("transform "
228 + lib.concatMapStringsSep "," toString (lib.flatten config.transform))
229 ++ lib.optional (config.scale != null)
230 ((if config.scale.method == "factor" then "scale" else "scale-from")
231 + " ${toString config.scale.x}x${toString config.scale.y}"))
241 services.autorandr = {
242 enable = lib.mkEnableOption "handling of hotplug and sleep events by autorandr";
244 defaultTarget = lib.mkOption {
246 type = lib.types.str;
248 Fallback if no monitor layout can be detected. See the docs
249 (https://github.com/phillipberndt/autorandr/blob/v1.0/README.md#how-to-use)
250 for further reference.
254 ignoreLid = lib.mkOption {
256 type = lib.types.bool;
257 description = "Treat outputs as connected even if their lids are closed";
260 matchEdid = lib.mkOption {
262 type = lib.types.bool;
263 description = "Match displays based on edid instead of name";
266 hooks = lib.mkOption {
268 description = "Global hook scripts";
270 example = lib.literalExpression ''
273 "notify-i3" = "''${pkgs.i3}/bin/i3-msg restart";
274 "change-background" = readFile ./change-background.sh;
276 case "$AUTORANDR_CURRENT_PROFILE" in
287 echo "Unknown profle: $AUTORANDR_CURRENT_PROFILE"
290 echo "Xft.dpi: $DPI" | ''${pkgs.xorg.xrdb}/bin/xrdb -merge
296 profiles = lib.mkOption {
297 type = lib.types.attrsOf profileModule;
298 description = "Autorandr profiles specification.";
300 example = lib.literalExpression ''
315 gamma = "1.0:0.909:0.833";
320 hooks.postswitch = readFile ./work-postswitch.sh;
330 config = lib.mkIf cfg.enable {
332 services.udev.packages = [ pkgs.autorandr ];
335 systemPackages = [ pkgs.autorandr ];
337 (lib.mapAttrs' (hookToFile "postswitch.d") cfg.hooks.postswitch)
338 (lib.mapAttrs' (hookToFile "preswitch.d") cfg.hooks.preswitch)
339 (lib.mapAttrs' (hookToFile "predetect.d") cfg.hooks.predetect)
340 (lib.mkMerge (lib.mapAttrsToList profileToFiles cfg.profiles))
344 systemd.services.autorandr = {
345 wantedBy = [ "sleep.target" ];
346 description = "Autorandr execution hook";
347 after = [ "sleep.target" ];
349 startLimitIntervalSec = 5;
353 ${pkgs.autorandr}/bin/autorandr \
356 --default ${cfg.defaultTarget} \
357 ${lib.optionalString cfg.ignoreLid "--ignore-lid"} \
358 ${lib.optionalString cfg.matchEdid "--match-edid"}
361 RemainAfterExit = false;
362 KillMode = "process";
368 meta.maintainers = with lib.maintainers; [ alexnortung ];