python3Packages.orjson: Disable failing tests on 32 bit
[NixPkgs.git] / nixos / modules / services / misc / autorandr.nix
blob365fdd5fcc3986585f2a95cf10a0873c2d9a36f5
1 { config, lib, pkgs, ... }:
3 with lib;
5 let
7   cfg = config.services.autorandr;
8   hookType = types.lines;
10   matrixOf = n: m: elemType:
11   mkOptionType rec {
12     name = "matrixOf";
13     description =
14       "${toString n}×${toString m} matrix of ${elemType.description}s";
15     check = xss:
16       let listOfSize = l: xs: isList xs && length xs == l;
17       in listOfSize n xss
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; };
24   };
26   profileModule = types.submodule {
27     options = {
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.
33         '';
34         default = { };
35       };
37       config = mkOption {
38         type = types.attrsOf configModule;
39         description = lib.mdDoc "Per output profile configuration.";
40         default = { };
41       };
43       hooks = mkOption {
44         type = hooksModule;
45         description = lib.mdDoc "Profile hook scripts.";
46         default = { };
47       };
48     };
49   };
51   configModule = types.submodule {
52     options = {
53       enable = mkOption {
54         type = types.bool;
55         description = lib.mdDoc "Whether to enable the output.";
56         default = true;
57       };
59       crtc = mkOption {
60         type = types.nullOr types.ints.unsigned;
61         description = lib.mdDoc "Output video display controller.";
62         default = null;
63         example = 0;
64       };
66       primary = mkOption {
67         type = types.bool;
68         description = lib.mdDoc "Whether output should be marked as primary";
69         default = false;
70       };
72       position = mkOption {
73         type = types.str;
74         description = lib.mdDoc "Output position";
75         default = "";
76         example = "5760x0";
77       };
79       mode = mkOption {
80         type = types.str;
81         description = lib.mdDoc "Output resolution.";
82         default = "";
83         example = "3840x2160";
84       };
86       rate = mkOption {
87         type = types.str;
88         description = lib.mdDoc "Output framerate.";
89         default = "";
90         example = "60.00";
91       };
93       gamma = mkOption {
94         type = types.str;
95         description = lib.mdDoc "Output gamma configuration.";
96         default = "";
97         example = "1.0:0.909:0.833";
98       };
100       rotate = mkOption {
101         type = types.nullOr (types.enum [ "normal" "left" "right" "inverted" ]);
102         description = lib.mdDoc "Output rotate configuration.";
103         default = null;
104         example = "left";
105       };
107       transform = mkOption {
108         type = types.nullOr (matrixOf 3 3 types.float);
109         default = null;
110         example = literalExpression ''
111           [
112             [ 0.6 0.0 0.0 ]
113             [ 0.0 0.6 0.0 ]
114             [ 0.0 0.0 1.0 ]
115           ]
116         '';
117         description = lib.mdDoc ''
118           Refer to
119           {manpage}`xrandr(1)`
120           for the documentation of the transform matrix.
121         '';
122       };
124       dpi = mkOption {
125         type = types.nullOr types.ints.positive;
126         description = lib.mdDoc "Output DPI configuration.";
127         default = null;
128         example = 96;
129       };
131       scale = mkOption {
132         type = types.nullOr (types.submodule {
133           options = {
134             method = mkOption {
135               type = types.enum [ "factor" "pixel" ];
136               description = lib.mdDoc "Output scaling method.";
137               default = "factor";
138               example = "pixel";
139             };
141             x = mkOption {
142               type = types.either types.float types.ints.positive;
143               description = lib.mdDoc "Horizontal scaling factor/pixels.";
144             };
146             y = mkOption {
147               type = types.either types.float types.ints.positive;
148               description = lib.mdDoc "Vertical scaling factor/pixels.";
149             };
150           };
151         });
152         description = lib.mdDoc ''
153           Output scale configuration.
155           Either configure by pixels or a scaling factor. When using pixel method the
156           {manpage}`xrandr(1)`
157           option
158           `--scale-from`
159           will be used; when using factor method the option
160           `--scale`
161           will be used.
163           This option is a shortcut version of the transform option and they are mutually
164           exclusive.
165         '';
166         default = null;
167         example = literalExpression ''
168           {
169             x = 1.25;
170             y = 1.25;
171           }
172         '';
173       };
174     };
175   };
177   hooksModule = types.submodule {
178     options = {
179       postswitch = mkOption {
180         type = types.attrsOf hookType;
181         description = lib.mdDoc "Postswitch hook executed after mode switch.";
182         default = { };
183       };
185       preswitch = mkOption {
186         type = types.attrsOf hookType;
187         description = lib.mdDoc "Preswitch hook executed before mode switch.";
188         default = { };
189       };
191       predetect = mkOption {
192         type = types.attrsOf hookType;
193         description = lib.mdDoc ''
194           Predetect hook executed before autorandr attempts to run xrandr.
195         '';
196         default = { };
197       };
198     };
199   };
201   hookToFile = folder: name: hook:
202     nameValuePair "xdg/autorandr/${folder}/${name}" {
203       source = "${pkgs.writeShellScriptBin "hook" hook}/bin/hook";
204     };
205   profileToFiles = name: profile:
206     with profile;
207     mkMerge ([
208       {
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);
213       }
214       (mapAttrs' (hookToFile "${name}/postswitch.d") hooks.postswitch)
215       (mapAttrs' (hookToFile "${name}/preswitch.d") hooks.preswitch)
216       (mapAttrs' (hookToFile "${name}/predetect.d") hooks.predetect)
217     ]);
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}"))
235     else ''
236       output ${name}
237       off
238     '';
240 in {
242   options = {
244     services.autorandr = {
245       enable = mkEnableOption (lib.mdDoc "handling of hotplug and sleep events by autorandr");
247       defaultTarget = mkOption {
248         default = "default";
249         type = types.str;
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.
254         '';
255       };
257       hooks = mkOption {
258         type = hooksModule;
259         description = lib.mdDoc "Global hook scripts";
260         default = { };
261         example = ''
262           {
263             postswitch = {
264               "notify-i3" = "''${pkgs.i3}/bin/i3-msg restart";
265               "change-background" = readFile ./change-background.sh;
266               "change-dpi" = '''
267                 case "$AUTORANDR_CURRENT_PROFILE" in
268                   default)
269                     DPI=120
270                     ;;
271                   home)
272                     DPI=192
273                     ;;
274                   work)
275                     DPI=144
276                     ;;
277                   *)
278                     echo "Unknown profle: $AUTORANDR_CURRENT_PROFILE"
279                     exit 1
280                 esac
281                 echo "Xft.dpi: $DPI" | ''${pkgs.xorg.xrdb}/bin/xrdb -merge
282               '''
283             };
284           }
285         '';
286       };
287       profiles = mkOption {
288         type = types.attrsOf profileModule;
289         description = lib.mdDoc "Autorandr profiles specification.";
290         default = { };
291         example = literalExpression ''
292           {
293             "work" = {
294               fingerprint = {
295                 eDP1 = "<EDID>";
296                 DP1 = "<EDID>";
297               };
298               config = {
299                 eDP1.enable = false;
300                 DP1 = {
301                   enable = true;
302                   crtc = 0;
303                   primary = true;
304                   position = "0x0";
305                   mode = "3840x2160";
306                   gamma = "1.0:0.909:0.833";
307                   rate = "60.00";
308                   rotate = "left";
309                 };
310               };
311               hooks.postswitch = readFile ./work-postswitch.sh;
312             };
313           }
314         '';
315       };
317     };
319   };
321   config = mkIf cfg.enable {
323     services.udev.packages = [ pkgs.autorandr ];
325     environment = {
326       systemPackages = [ pkgs.autorandr ];
327       etc = mkMerge ([
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))
332       ]);
333     };
335     systemd.services.autorandr = {
336       wantedBy = [ "sleep.target" ];
337       description = "Autorandr execution hook";
338       after = [ "sleep.target" ];
340       startLimitIntervalSec = 5;
341       startLimitBurst = 1;
342       serviceConfig = {
343         ExecStart = "${pkgs.autorandr}/bin/autorandr --batch --change --default ${cfg.defaultTarget}";
344         Type = "oneshot";
345         RemainAfterExit = false;
346         KillMode = "process";
347       };
348     };
350   };
352   meta.maintainers = with maintainers; [ alexnortung ];