grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / misc / autorandr.nix
blobf0490d63ad7fc3380aac566817b2fac2ee59cfc7
1 { config, lib, pkgs, ... }:
2 let
4   cfg = config.services.autorandr;
5   hookType = lib.types.lines;
7   matrixOf = n: m: elemType:
8   lib.mkOptionType rec {
9     name = "matrixOf";
10     description =
11       "${toString n}×${toString m} matrix of ${elemType.description}s";
12     check = xss:
13       let listOfSize = l: xs: lib.isList xs && lib.length xs == l;
14       in listOfSize n xss
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; };
21   };
23   profileModule = lib.types.submodule {
24     options = {
25       fingerprint = lib.mkOption {
26         type = lib.types.attrsOf lib.types.str;
27         description = ''
28           Output name to EDID mapping.
29           Use `autorandr --fingerprint` to get current setup values.
30         '';
31         default = { };
32       };
34       config = lib.mkOption {
35         type = lib.types.attrsOf configModule;
36         description = "Per output profile configuration.";
37         default = { };
38       };
40       hooks = lib.mkOption {
41         type = hooksModule;
42         description = "Profile hook scripts.";
43         default = { };
44       };
45     };
46   };
48   configModule = lib.types.submodule {
49     options = {
50       enable = lib.mkOption {
51         type = lib.types.bool;
52         description = "Whether to enable the output.";
53         default = true;
54       };
56       crtc = lib.mkOption {
57         type = lib.types.nullOr lib.types.ints.unsigned;
58         description = "Output video display controller.";
59         default = null;
60         example = 0;
61       };
63       primary = lib.mkOption {
64         type = lib.types.bool;
65         description = "Whether output should be marked as primary";
66         default = false;
67       };
69       position = lib.mkOption {
70         type = lib.types.str;
71         description = "Output position";
72         default = "";
73         example = "5760x0";
74       };
76       mode = lib.mkOption {
77         type = lib.types.str;
78         description = "Output resolution.";
79         default = "";
80         example = "3840x2160";
81       };
83       rate = lib.mkOption {
84         type = lib.types.str;
85         description = "Output framerate.";
86         default = "";
87         example = "60.00";
88       };
90       gamma = lib.mkOption {
91         type = lib.types.str;
92         description = "Output gamma configuration.";
93         default = "";
94         example = "1.0:0.909:0.833";
95       };
97       rotate = lib.mkOption {
98         type = lib.types.nullOr (lib.types.enum [ "normal" "left" "right" "inverted" ]);
99         description = "Output rotate configuration.";
100         default = null;
101         example = "left";
102       };
104       transform = lib.mkOption {
105         type = lib.types.nullOr (matrixOf 3 3 lib.types.float);
106         default = null;
107         example = lib.literalExpression ''
108           [
109             [ 0.6 0.0 0.0 ]
110             [ 0.0 0.6 0.0 ]
111             [ 0.0 0.0 1.0 ]
112           ]
113         '';
114         description = ''
115           Refer to
116           {manpage}`xrandr(1)`
117           for the documentation of the transform matrix.
118         '';
119       };
121       dpi = lib.mkOption {
122         type = lib.types.nullOr lib.types.ints.positive;
123         description = "Output DPI configuration.";
124         default = null;
125         example = 96;
126       };
128       scale = lib.mkOption {
129         type = lib.types.nullOr (lib.types.submodule {
130           options = {
131             method = lib.mkOption {
132               type = lib.types.enum [ "factor" "pixel" ];
133               description = "Output scaling method.";
134               default = "factor";
135               example = "pixel";
136             };
138             x = lib.mkOption {
139               type = lib.types.either lib.types.float lib.types.ints.positive;
140               description = "Horizontal scaling factor/pixels.";
141             };
143             y = lib.mkOption {
144               type = lib.types.either lib.types.float lib.types.ints.positive;
145               description = "Vertical scaling factor/pixels.";
146             };
147           };
148         });
149         description = ''
150           Output scale configuration.
152           Either configure by pixels or a scaling factor. When using pixel method the
153           {manpage}`xrandr(1)`
154           option
155           `--scale-from`
156           will be used; when using factor method the option
157           `--scale`
158           will be used.
160           This option is a shortcut version of the transform option and they are mutually
161           exclusive.
162         '';
163         default = null;
164         example = lib.literalExpression ''
165           {
166             x = 1.25;
167             y = 1.25;
168           }
169         '';
170       };
171     };
172   };
174   hooksModule = lib.types.submodule {
175     options = {
176       postswitch = lib.mkOption {
177         type = lib.types.attrsOf hookType;
178         description = "Postswitch hook executed after mode switch.";
179         default = { };
180       };
182       preswitch = lib.mkOption {
183         type = lib.types.attrsOf hookType;
184         description = "Preswitch hook executed before mode switch.";
185         default = { };
186       };
188       predetect = lib.mkOption {
189         type = lib.types.attrsOf hookType;
190         description = ''
191           Predetect hook executed before autorandr attempts to run xrandr.
192         '';
193         default = { };
194       };
195     };
196   };
198   hookToFile = folder: name: hook:
199     lib.nameValuePair "xdg/autorandr/${folder}/${name}" {
200       source = "${pkgs.writeShellScriptBin "hook" hook}/bin/hook";
201     };
202   profileToFiles = name: profile:
203     with profile;
204     lib.mkMerge ([
205       {
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);
210       }
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)
214     ]);
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}"))
232     else ''
233       output ${name}
234       off
235     '';
237 in {
239   options = {
241     services.autorandr = {
242       enable = lib.mkEnableOption "handling of hotplug and sleep events by autorandr";
244       defaultTarget = lib.mkOption {
245         default = "default";
246         type = lib.types.str;
247         description = ''
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.
251         '';
252       };
254       ignoreLid = lib.mkOption {
255         default = false;
256         type = lib.types.bool;
257         description = "Treat outputs as connected even if their lids are closed";
258       };
260       matchEdid = lib.mkOption {
261         default = false;
262         type = lib.types.bool;
263         description = "Match displays based on edid instead of name";
264       };
266       hooks = lib.mkOption {
267         type = hooksModule;
268         description = "Global hook scripts";
269         default = { };
270         example = lib.literalExpression ''
271           {
272             postswitch = {
273               "notify-i3" = "''${pkgs.i3}/bin/i3-msg restart";
274               "change-background" = readFile ./change-background.sh;
275               "change-dpi" = '''
276                 case "$AUTORANDR_CURRENT_PROFILE" in
277                   default)
278                     DPI=120
279                     ;;
280                   home)
281                     DPI=192
282                     ;;
283                   work)
284                     DPI=144
285                     ;;
286                   *)
287                     echo "Unknown profle: $AUTORANDR_CURRENT_PROFILE"
288                     exit 1
289                 esac
290                 echo "Xft.dpi: $DPI" | ''${pkgs.xorg.xrdb}/bin/xrdb -merge
291               ''';
292             };
293           }
294         '';
295       };
296       profiles = lib.mkOption {
297         type = lib.types.attrsOf profileModule;
298         description = "Autorandr profiles specification.";
299         default = { };
300         example = lib.literalExpression ''
301           {
302             "work" = {
303               fingerprint = {
304                 eDP1 = "<EDID>";
305                 DP1 = "<EDID>";
306               };
307               config = {
308                 eDP1.enable = false;
309                 DP1 = {
310                   enable = true;
311                   crtc = 0;
312                   primary = true;
313                   position = "0x0";
314                   mode = "3840x2160";
315                   gamma = "1.0:0.909:0.833";
316                   rate = "60.00";
317                   rotate = "left";
318                 };
319               };
320               hooks.postswitch = readFile ./work-postswitch.sh;
321             };
322           }
323         '';
324       };
326     };
328   };
330   config = lib.mkIf cfg.enable {
332     services.udev.packages = [ pkgs.autorandr ];
334     environment = {
335       systemPackages = [ pkgs.autorandr ];
336       etc = lib.mkMerge ([
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))
341       ]);
342     };
344     systemd.services.autorandr = {
345       wantedBy = [ "sleep.target" ];
346       description = "Autorandr execution hook";
347       after = [ "sleep.target" ];
349       startLimitIntervalSec = 5;
350       startLimitBurst = 1;
351       serviceConfig = {
352         ExecStart = ''
353           ${pkgs.autorandr}/bin/autorandr \
354             --batch \
355             --change \
356             --default ${cfg.defaultTarget} \
357             ${lib.optionalString cfg.ignoreLid "--ignore-lid"} \
358             ${lib.optionalString cfg.matchEdid "--match-edid"}
359         '';
360         Type = "oneshot";
361         RemainAfterExit = false;
362         KillMode = "process";
363       };
364     };
366   };
368   meta.maintainers = with lib.maintainers; [ alexnortung ];