@@ -5,6 +5,243 @@ with lib;
55let
66
77 cfg = config.services.autorandr;
8+ hookType = types.lines;
9+
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+ };
25+
26+ profileModule = types.submodule {
27+ options = {
28+ fingerprint = mkOption {
29+ type = types.attrsOf types.str;
30+ description = ''
31+ Output name to EDID mapping.
32+ Use <code>autorandr --fingerprint</code> to get current setup values.
33+ '';
34+ default = { };
35+ };
36+
37+ config = mkOption {
38+ type = types.attrsOf configModule;
39+ description = "Per output profile configuration.";
40+ default = { };
41+ };
42+
43+ hooks = mkOption {
44+ type = hooksModule;
45+ description = "Profile hook scripts.";
46+ default = { };
47+ };
48+ };
49+ };
50+
51+ configModule = types.submodule {
52+ options = {
53+ enable = mkOption {
54+ type = types.bool;
55+ description = "Whether to enable the output.";
56+ default = true;
57+ };
58+
59+ crtc = mkOption {
60+ type = types.nullOr types.ints.unsigned;
61+ description = "Output video display controller.";
62+ default = null;
63+ example = 0;
64+ };
65+
66+ primary = mkOption {
67+ type = types.bool;
68+ description = "Whether output should be marked as primary";
69+ default = false;
70+ };
71+
72+ position = mkOption {
73+ type = types.str;
74+ description = "Output position";
75+ default = "";
76+ example = "5760x0";
77+ };
78+
79+ mode = mkOption {
80+ type = types.str;
81+ description = "Output resolution.";
82+ default = "";
83+ example = "3840x2160";
84+ };
85+
86+ rate = mkOption {
87+ type = types.str;
88+ description = "Output framerate.";
89+ default = "";
90+ example = "60.00";
91+ };
92+
93+ gamma = mkOption {
94+ type = types.str;
95+ description = "Output gamma configuration.";
96+ default = "";
97+ example = "1.0:0.909:0.833";
98+ };
99+
100+ rotate = mkOption {
101+ type = types.nullOr (types.enum [ "normal" "left" "right" "inverted" ]);
102+ description = "Output rotate configuration.";
103+ default = null;
104+ example = "left";
105+ };
106+
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 = ''
118+ Refer to
119+ <citerefentry>
120+ <refentrytitle>xrandr</refentrytitle>
121+ <manvolnum>1</manvolnum>
122+ </citerefentry>
123+ for the documentation of the transform matrix.
124+ '';
125+ };
126+
127+ dpi = mkOption {
128+ type = types.nullOr types.ints.positive;
129+ description = "Output DPI configuration.";
130+ default = null;
131+ example = 96;
132+ };
133+
134+ scale = mkOption {
135+ type = types.nullOr (types.submodule {
136+ options = {
137+ method = mkOption {
138+ type = types.enum [ "factor" "pixel" ];
139+ description = "Output scaling method.";
140+ default = "factor";
141+ example = "pixel";
142+ };
143+
144+ x = mkOption {
145+ type = types.either types.float types.ints.positive;
146+ description = "Horizontal scaling factor/pixels.";
147+ };
148+
149+ y = mkOption {
150+ type = types.either types.float types.ints.positive;
151+ description = "Vertical scaling factor/pixels.";
152+ };
153+ };
154+ });
155+ description = ''
156+ Output scale configuration.
157+ </para><para>
158+ Either configure by pixels or a scaling factor. When using pixel method the
159+ <citerefentry>
160+ <refentrytitle>xrandr</refentrytitle>
161+ <manvolnum>1</manvolnum>
162+ </citerefentry>
163+ option
164+ <parameter class="command">--scale-from</parameter>
165+ will be used; when using factor method the option
166+ <parameter class="command">--scale</parameter>
167+ will be used.
168+ </para><para>
169+ This option is a shortcut version of the transform option and they are mutually
170+ exclusive.
171+ '';
172+ default = null;
173+ example = literalExpression ''
174+ {
175+ x = 1.25;
176+ y = 1.25;
177+ }
178+ '';
179+ };
180+ };
181+ };
182+
183+ hooksModule = types.submodule {
184+ options = {
185+ postswitch = mkOption {
186+ type = types.attrsOf hookType;
187+ description = "Postswitch hook executed after mode switch.";
188+ default = { };
189+ };
190+
191+ preswitch = mkOption {
192+ type = types.attrsOf hookType;
193+ description = "Preswitch hook executed before mode switch.";
194+ default = { };
195+ };
196+
197+ predetect = mkOption {
198+ type = types.attrsOf hookType;
199+ description = ''
200+ Predetect hook executed before autorandr attempts to run xrandr.
201+ '';
202+ default = { };
203+ };
204+ };
205+ };
206+
207+ hookToFile = folder: name: hook:
208+ nameValuePair "xdg/autorandr/${folder}/${name}" {
209+ source = "${pkgs.writeShellScriptBin "hook" hook}/bin/hook";
210+ };
211+ profileToFiles = name: profile:
212+ with profile;
213+ mkMerge ([
214+ {
215+ "xdg/autorandr/${name}/setup".text = concatStringsSep "\n"
216+ (mapAttrsToList fingerprintToString fingerprint);
217+ "xdg/autorandr/${name}/config".text =
218+ concatStringsSep "\n" (mapAttrsToList configToString profile.config);
219+ }
220+ (mapAttrs' (hookToFile "${name}/postswitch.d") hooks.postswitch)
221+ (mapAttrs' (hookToFile "${name}/preswitch.d") hooks.preswitch)
222+ (mapAttrs' (hookToFile "${name}/predetect.d") hooks.predetect)
223+ ]);
224+ fingerprintToString = name: edid: "${name} ${edid}";
225+ configToString = name: config:
226+ if config.enable then
227+ concatStringsSep "\n" ([ "output ${name}" ]
228+ ++ optional (config.position != "") "pos ${config.position}"
229+ ++ optional (config.crtc != null) "crtc ${toString config.crtc}"
230+ ++ optional config.primary "primary"
231+ ++ optional (config.dpi != null) "dpi ${toString config.dpi}"
232+ ++ optional (config.gamma != "") "gamma ${config.gamma}"
233+ ++ optional (config.mode != "") "mode ${config.mode}"
234+ ++ optional (config.rate != "") "rate ${config.rate}"
235+ ++ optional (config.rotate != null) "rotate ${config.rotate}"
236+ ++ optional (config.transform != null) ("transform "
237+ + concatMapStringsSep "," toString (flatten config.transform))
238+ ++ optional (config.scale != null)
239+ ((if config.scale.method == "factor" then "scale" else "scale-from")
240+ + " ${toString config.scale.x}x${toString config.scale.y}"))
241+ else ''
242+ output ${name}
243+ off
244+ '';
8245
9246in {
10247
@@ -22,6 +259,67 @@ in {
22259 for further reference.
23260 '';
24261 };
262+
263+ hooks = mkOption {
264+ type = hooksModule;
265+ description = "Global hook scripts";
266+ default = { };
267+ example = ''
268+ {
269+ postswitch = {
270+ "notify-i3" = "''${pkgs.i3}/bin/i3-msg restart";
271+ "change-background" = readFile ./change-background.sh;
272+ "change-dpi" = '''
273+ case "$AUTORANDR_CURRENT_PROFILE" in
274+ default)
275+ DPI=120
276+ ;;
277+ home)
278+ DPI=192
279+ ;;
280+ work)
281+ DPI=144
282+ ;;
283+ *)
284+ echo "Unknown profle: $AUTORANDR_CURRENT_PROFILE"
285+ exit 1
286+ esac
287+ echo "Xft.dpi: $DPI" | ''${pkgs.xorg.xrdb}/bin/xrdb -merge
288+ '''
289+ };
290+ }
291+ '';
292+ };
293+ profiles = mkOption {
294+ type = types.attrsOf profileModule;
295+ description = "Autorandr profiles specification.";
296+ default = { };
297+ example = literalExpression ''
298+ {
299+ "work" = {
300+ fingerprint = {
301+ eDP1 = "<EDID>";
302+ DP1 = "<EDID>";
303+ };
304+ config = {
305+ eDP1.enable = false;
306+ DP1 = {
307+ enable = true;
308+ crtc = 0;
309+ primary = true;
310+ position = "0x0";
311+ mode = "3840x2160";
312+ gamma = "1.0:0.909:0.833";
313+ rate = "60.00";
314+ rotate = "left";
315+ };
316+ };
317+ hooks.postswitch = readFile ./work-postswitch.sh;
318+ };
319+ }
320+ '';
321+ };
322+
25323 };
26324
27325 };
@@ -30,7 +328,15 @@ in {
30328
31329 services.udev.packages = [ pkgs.autorandr ];
32330
33− environment.systemPackages = [ pkgs.autorandr ];
331+ environment = {
332+ systemPackages = [ pkgs.autorandr ];
333+ etc = mkMerge ([
334+ (mapAttrs' (hookToFile "postswitch.d") cfg.hooks.postswitch)
335+ (mapAttrs' (hookToFile "preswitch.d") cfg.hooks.preswitch)
336+ (mapAttrs' (hookToFile "predetect.d") cfg.hooks.predetect)
337+ (mkMerge (mapAttrsToList profileToFiles cfg.profiles))
338+ ]);
339+ };
34340
35341 systemd.services.autorandr = {
36342 wantedBy = [ "sleep.target" ];
@@ -49,5 +355,5 @@ in {
49355
50356 };
51357
52− meta.maintainers = with maintainers; [ ];
358+ meta.maintainers = with maintainers; [ alexnortung ];
53359}