at 25.11-pre 12 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 9 cfg = config.services.autorandr; 10 hookType = lib.types.lines; 11 12 matrixOf = 13 n: m: elemType: 14 lib.mkOptionType rec { 15 name = "matrixOf"; 16 description = "${toString n}×${toString m} matrix of ${elemType.description}s"; 17 check = 18 xss: 19 let 20 listOfSize = l: xs: lib.isList xs && lib.length xs == l; 21 in 22 listOfSize n xss && lib.all (xs: listOfSize m xs && lib.all elemType.check xs) xss; 23 merge = lib.mergeOneOption; 24 getSubOptions = 25 prefix: 26 elemType.getSubOptions ( 27 prefix 28 ++ [ 29 "*" 30 "*" 31 ] 32 ); 33 getSubModules = elemType.getSubModules; 34 substSubModules = mod: matrixOf n m (elemType.substSubModules mod); 35 functor = (lib.defaultFunctor name) // { 36 wrapped = elemType; 37 }; 38 }; 39 40 profileModule = lib.types.submodule { 41 options = { 42 fingerprint = lib.mkOption { 43 type = lib.types.attrsOf lib.types.str; 44 description = '' 45 Output name to EDID mapping. 46 Use `autorandr --fingerprint` to get current setup values. 47 ''; 48 default = { }; 49 }; 50 51 config = lib.mkOption { 52 type = lib.types.attrsOf configModule; 53 description = "Per output profile configuration."; 54 default = { }; 55 }; 56 57 hooks = lib.mkOption { 58 type = hooksModule; 59 description = "Profile hook scripts."; 60 default = { }; 61 }; 62 }; 63 }; 64 65 configModule = lib.types.submodule { 66 options = { 67 enable = lib.mkOption { 68 type = lib.types.bool; 69 description = "Whether to enable the output."; 70 default = true; 71 }; 72 73 crtc = lib.mkOption { 74 type = lib.types.nullOr lib.types.ints.unsigned; 75 description = "Output video display controller."; 76 default = null; 77 example = 0; 78 }; 79 80 primary = lib.mkOption { 81 type = lib.types.bool; 82 description = "Whether output should be marked as primary"; 83 default = false; 84 }; 85 86 position = lib.mkOption { 87 type = lib.types.str; 88 description = "Output position"; 89 default = ""; 90 example = "5760x0"; 91 }; 92 93 mode = lib.mkOption { 94 type = lib.types.str; 95 description = "Output resolution."; 96 default = ""; 97 example = "3840x2160"; 98 }; 99 100 rate = lib.mkOption { 101 type = lib.types.str; 102 description = "Output framerate."; 103 default = ""; 104 example = "60.00"; 105 }; 106 107 gamma = lib.mkOption { 108 type = lib.types.str; 109 description = "Output gamma configuration."; 110 default = ""; 111 example = "1.0:0.909:0.833"; 112 }; 113 114 rotate = lib.mkOption { 115 type = lib.types.nullOr ( 116 lib.types.enum [ 117 "normal" 118 "left" 119 "right" 120 "inverted" 121 ] 122 ); 123 description = "Output rotate configuration."; 124 default = null; 125 example = "left"; 126 }; 127 128 transform = lib.mkOption { 129 type = lib.types.nullOr (matrixOf 3 3 lib.types.float); 130 default = null; 131 example = lib.literalExpression '' 132 [ 133 [ 0.6 0.0 0.0 ] 134 [ 0.0 0.6 0.0 ] 135 [ 0.0 0.0 1.0 ] 136 ] 137 ''; 138 description = '' 139 Refer to 140 {manpage}`xrandr(1)` 141 for the documentation of the transform matrix. 142 ''; 143 }; 144 145 dpi = lib.mkOption { 146 type = lib.types.nullOr lib.types.ints.positive; 147 description = "Output DPI configuration."; 148 default = null; 149 example = 96; 150 }; 151 152 scale = lib.mkOption { 153 type = lib.types.nullOr ( 154 lib.types.submodule { 155 options = { 156 method = lib.mkOption { 157 type = lib.types.enum [ 158 "factor" 159 "pixel" 160 ]; 161 description = "Output scaling method."; 162 default = "factor"; 163 example = "pixel"; 164 }; 165 166 x = lib.mkOption { 167 type = lib.types.either lib.types.float lib.types.ints.positive; 168 description = "Horizontal scaling factor/pixels."; 169 }; 170 171 y = lib.mkOption { 172 type = lib.types.either lib.types.float lib.types.ints.positive; 173 description = "Vertical scaling factor/pixels."; 174 }; 175 }; 176 } 177 ); 178 description = '' 179 Output scale configuration. 180 181 Either configure by pixels or a scaling factor. When using pixel method the 182 {manpage}`xrandr(1)` 183 option 184 `--scale-from` 185 will be used; when using factor method the option 186 `--scale` 187 will be used. 188 189 This option is a shortcut version of the transform option and they are mutually 190 exclusive. 191 ''; 192 default = null; 193 example = lib.literalExpression '' 194 { 195 x = 1.25; 196 y = 1.25; 197 } 198 ''; 199 }; 200 }; 201 }; 202 203 hooksModule = lib.types.submodule { 204 options = { 205 postswitch = lib.mkOption { 206 type = lib.types.attrsOf hookType; 207 description = "Postswitch hook executed after mode switch."; 208 default = { }; 209 }; 210 211 preswitch = lib.mkOption { 212 type = lib.types.attrsOf hookType; 213 description = "Preswitch hook executed before mode switch."; 214 default = { }; 215 }; 216 217 predetect = lib.mkOption { 218 type = lib.types.attrsOf hookType; 219 description = '' 220 Predetect hook executed before autorandr attempts to run xrandr. 221 ''; 222 default = { }; 223 }; 224 }; 225 }; 226 227 hookToFile = 228 folder: name: hook: 229 lib.nameValuePair "xdg/autorandr/${folder}/${name}" { 230 source = "${pkgs.writeShellScriptBin "hook" hook}/bin/hook"; 231 }; 232 profileToFiles = 233 name: profile: 234 with profile; 235 lib.mkMerge ([ 236 { 237 "xdg/autorandr/${name}/setup".text = lib.concatStringsSep "\n" ( 238 lib.mapAttrsToList fingerprintToString fingerprint 239 ); 240 "xdg/autorandr/${name}/config".text = lib.concatStringsSep "\n" ( 241 lib.mapAttrsToList configToString profile.config 242 ); 243 } 244 (lib.mapAttrs' (hookToFile "${name}/postswitch.d") hooks.postswitch) 245 (lib.mapAttrs' (hookToFile "${name}/preswitch.d") hooks.preswitch) 246 (lib.mapAttrs' (hookToFile "${name}/predetect.d") hooks.predetect) 247 ]); 248 fingerprintToString = name: edid: "${name} ${edid}"; 249 configToString = 250 name: config: 251 if config.enable then 252 lib.concatStringsSep "\n" ( 253 [ "output ${name}" ] 254 ++ lib.optional (config.position != "") "pos ${config.position}" 255 ++ lib.optional (config.crtc != null) "crtc ${toString config.crtc}" 256 ++ lib.optional config.primary "primary" 257 ++ lib.optional (config.dpi != null) "dpi ${toString config.dpi}" 258 ++ lib.optional (config.gamma != "") "gamma ${config.gamma}" 259 ++ lib.optional (config.mode != "") "mode ${config.mode}" 260 ++ lib.optional (config.rate != "") "rate ${config.rate}" 261 ++ lib.optional (config.rotate != null) "rotate ${config.rotate}" 262 ++ lib.optional (config.transform != null) ( 263 "transform " + lib.concatMapStringsSep "," toString (lib.flatten config.transform) 264 ) 265 ++ lib.optional (config.scale != null) ( 266 (if config.scale.method == "factor" then "scale" else "scale-from") 267 + " ${toString config.scale.x}x${toString config.scale.y}" 268 ) 269 ) 270 else 271 '' 272 output ${name} 273 off 274 ''; 275 276in 277{ 278 279 options = { 280 281 services.autorandr = { 282 enable = lib.mkEnableOption "handling of hotplug and sleep events by autorandr"; 283 284 defaultTarget = lib.mkOption { 285 default = "default"; 286 type = lib.types.str; 287 description = '' 288 Fallback if no monitor layout can be detected. See the docs 289 (https://github.com/phillipberndt/autorandr/blob/v1.0/README.md#how-to-use) 290 for further reference. 291 ''; 292 }; 293 294 ignoreLid = lib.mkOption { 295 default = false; 296 type = lib.types.bool; 297 description = "Treat outputs as connected even if their lids are closed"; 298 }; 299 300 matchEdid = lib.mkOption { 301 default = false; 302 type = lib.types.bool; 303 description = "Match displays based on edid instead of name"; 304 }; 305 306 hooks = lib.mkOption { 307 type = hooksModule; 308 description = "Global hook scripts"; 309 default = { }; 310 example = lib.literalExpression '' 311 { 312 postswitch = { 313 "notify-i3" = "''${pkgs.i3}/bin/i3-msg restart"; 314 "change-background" = readFile ./change-background.sh; 315 "change-dpi" = ''' 316 case "$AUTORANDR_CURRENT_PROFILE" in 317 default) 318 DPI=120 319 ;; 320 home) 321 DPI=192 322 ;; 323 work) 324 DPI=144 325 ;; 326 *) 327 echo "Unknown profle: $AUTORANDR_CURRENT_PROFILE" 328 exit 1 329 esac 330 echo "Xft.dpi: $DPI" | ''${pkgs.xorg.xrdb}/bin/xrdb -merge 331 '''; 332 }; 333 } 334 ''; 335 }; 336 profiles = lib.mkOption { 337 type = lib.types.attrsOf profileModule; 338 description = "Autorandr profiles specification."; 339 default = { }; 340 example = lib.literalExpression '' 341 { 342 "work" = { 343 fingerprint = { 344 eDP1 = "<EDID>"; 345 DP1 = "<EDID>"; 346 }; 347 config = { 348 eDP1.enable = false; 349 DP1 = { 350 enable = true; 351 crtc = 0; 352 primary = true; 353 position = "0x0"; 354 mode = "3840x2160"; 355 gamma = "1.0:0.909:0.833"; 356 rate = "60.00"; 357 rotate = "left"; 358 }; 359 }; 360 hooks.postswitch = readFile ./work-postswitch.sh; 361 }; 362 } 363 ''; 364 }; 365 366 }; 367 368 }; 369 370 config = lib.mkIf cfg.enable { 371 372 services.udev.packages = [ pkgs.autorandr ]; 373 374 environment = { 375 systemPackages = [ pkgs.autorandr ]; 376 etc = lib.mkMerge ([ 377 (lib.mapAttrs' (hookToFile "postswitch.d") cfg.hooks.postswitch) 378 (lib.mapAttrs' (hookToFile "preswitch.d") cfg.hooks.preswitch) 379 (lib.mapAttrs' (hookToFile "predetect.d") cfg.hooks.predetect) 380 (lib.mkMerge (lib.mapAttrsToList profileToFiles cfg.profiles)) 381 ]); 382 }; 383 384 systemd.services.autorandr = { 385 wantedBy = [ "sleep.target" ]; 386 description = "Autorandr execution hook"; 387 after = [ "sleep.target" ]; 388 389 startLimitIntervalSec = 5; 390 startLimitBurst = 1; 391 serviceConfig = { 392 ExecStart = '' 393 ${pkgs.autorandr}/bin/autorandr \ 394 --batch \ 395 --change \ 396 --default ${cfg.defaultTarget} \ 397 ${lib.optionalString cfg.ignoreLid "--ignore-lid"} \ 398 ${lib.optionalString cfg.matchEdid "--match-edid"} 399 ''; 400 Type = "oneshot"; 401 RemainAfterExit = false; 402 KillMode = "process"; 403 }; 404 }; 405 406 }; 407 408 meta.maintainers = with lib.maintainers; [ alexnortung ]; 409}