at 23.11-pre 11 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 7 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 = lib.mdDoc '' 31 Output name to EDID mapping. 32 Use `autorandr --fingerprint` to get current setup values. 33 ''; 34 default = { }; 35 }; 36 37 config = mkOption { 38 type = types.attrsOf configModule; 39 description = lib.mdDoc "Per output profile configuration."; 40 default = { }; 41 }; 42 43 hooks = mkOption { 44 type = hooksModule; 45 description = lib.mdDoc "Profile hook scripts."; 46 default = { }; 47 }; 48 }; 49 }; 50 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 }; 58 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 }; 65 66 primary = mkOption { 67 type = types.bool; 68 description = lib.mdDoc "Whether output should be marked as primary"; 69 default = false; 70 }; 71 72 position = mkOption { 73 type = types.str; 74 description = lib.mdDoc "Output position"; 75 default = ""; 76 example = "5760x0"; 77 }; 78 79 mode = mkOption { 80 type = types.str; 81 description = lib.mdDoc "Output resolution."; 82 default = ""; 83 example = "3840x2160"; 84 }; 85 86 rate = mkOption { 87 type = types.str; 88 description = lib.mdDoc "Output framerate."; 89 default = ""; 90 example = "60.00"; 91 }; 92 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 }; 99 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 }; 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 = lib.mdDoc '' 118 Refer to 119 {manpage}`xrandr(1)` 120 for the documentation of the transform matrix. 121 ''; 122 }; 123 124 dpi = mkOption { 125 type = types.nullOr types.ints.positive; 126 description = lib.mdDoc "Output DPI configuration."; 127 default = null; 128 example = 96; 129 }; 130 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 }; 140 141 x = mkOption { 142 type = types.either types.float types.ints.positive; 143 description = lib.mdDoc "Horizontal scaling factor/pixels."; 144 }; 145 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. 154 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. 162 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 }; 176 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 }; 184 185 preswitch = mkOption { 186 type = types.attrsOf hookType; 187 description = lib.mdDoc "Preswitch hook executed before mode switch."; 188 default = { }; 189 }; 190 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 }; 200 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 ''; 239 240in { 241 242 options = { 243 244 services.autorandr = { 245 enable = mkEnableOption (lib.mdDoc "handling of hotplug and sleep events by autorandr"); 246 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 }; 256 257 ignoreLid = mkOption { 258 default = false; 259 type = types.bool; 260 description = lib.mdDoc "Treat outputs as connected even if their lids are closed"; 261 }; 262 263 hooks = mkOption { 264 type = hooksModule; 265 description = lib.mdDoc "Global hook scripts"; 266 default = { }; 267 example = literalExpression '' 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 = lib.mdDoc "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 323 }; 324 325 }; 326 327 config = mkIf cfg.enable { 328 329 services.udev.packages = [ pkgs.autorandr ]; 330 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 }; 340 341 systemd.services.autorandr = { 342 wantedBy = [ "sleep.target" ]; 343 description = "Autorandr execution hook"; 344 after = [ "sleep.target" ]; 345 346 startLimitIntervalSec = 5; 347 startLimitBurst = 1; 348 serviceConfig = { 349 ExecStart = '' 350 ${pkgs.autorandr}/bin/autorandr \ 351 --batch \ 352 --change \ 353 --default ${cfg.defaultTarget} \ 354 ${optionalString cfg.ignoreLid "--ignore-lid"} 355 ''; 356 Type = "oneshot"; 357 RemainAfterExit = false; 358 KillMode = "process"; 359 }; 360 }; 361 362 }; 363 364 meta.maintainers = with maintainers; [ alexnortung ]; 365}