home-manager/modules/programs/autorandr.nix

367 lines
10 KiB
Nix
Raw Normal View History

2018-04-09 15:04:43 +02:00
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.programs.autorandr;
2020-02-02 00:39:17 +01:00
matrixOf = n: m: elemType:
mkOptionType rec {
name = "matrixOf";
description =
"${toString n}×${toString m} matrix of ${elemType.description}s";
check = xss:
let listOfSize = l: xs: isList xs && length xs == l;
in listOfSize n xss
&& all (xs: listOfSize m xs && all elemType.check xs) xss;
merge = mergeOneOption;
getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "*" "*" ]);
getSubModules = elemType.getSubModules;
substSubModules = mod: matrixOf n m (elemType.substSubModules mod);
functor = (defaultFunctor name) // { wrapped = elemType; };
};
2018-04-09 15:04:43 +02:00
profileModule = types.submodule {
options = {
fingerprint = mkOption {
type = types.attrsOf types.str;
description = ''
2018-04-09 15:04:43 +02:00
Output name to EDID mapping.
Use `autorandr --fingerprint` to get current setup values.
2018-04-09 15:04:43 +02:00
'';
2020-02-02 00:39:17 +01:00
default = { };
2018-04-09 15:04:43 +02:00
};
config = mkOption {
type = types.attrsOf configModule;
description = "Per output profile configuration.";
2020-02-02 00:39:17 +01:00
default = { };
2018-04-09 15:04:43 +02:00
};
hooks = mkOption {
type = profileHooksModule;
description = "Profile hook scripts.";
2020-02-02 00:39:17 +01:00
default = { };
2018-04-09 15:04:43 +02:00
};
};
};
configModule = types.submodule {
options = {
enable = mkOption {
type = types.bool;
description = "Whether to enable the output.";
2018-04-09 15:04:43 +02:00
default = true;
};
crtc = mkOption {
type = types.nullOr types.ints.unsigned;
description = "Output video display controller.";
default = null;
example = 0;
};
2018-04-09 15:04:43 +02:00
primary = mkOption {
type = types.bool;
description = "Whether output should be marked as primary";
2018-04-09 15:04:43 +02:00
default = false;
};
position = mkOption {
type = types.str;
description = "Output position";
2018-04-09 15:04:43 +02:00
default = "";
example = "5760x0";
};
mode = mkOption {
type = types.str;
description = "Output resolution.";
2018-04-09 15:04:43 +02:00
default = "";
example = "3840x2160";
};
rate = mkOption {
type = types.str;
description = "Output framerate.";
2018-04-09 15:04:43 +02:00
default = "";
example = "60.00";
};
gamma = mkOption {
type = types.str;
description = "Output gamma configuration.";
2018-04-09 15:04:43 +02:00
default = "";
example = "1.0:0.909:0.833";
};
2018-06-25 21:24:36 +02:00
rotate = mkOption {
2020-02-02 00:39:17 +01:00
type = types.nullOr (types.enum [ "normal" "left" "right" "inverted" ]);
description = "Output rotate configuration.";
2018-06-25 21:24:36 +02:00
default = null;
example = "left";
};
transform = mkOption {
type = types.nullOr (matrixOf 3 3 types.float);
default = null;
example = literalExpression ''
[
[ 0.6 0.0 0.0 ]
[ 0.0 0.6 0.0 ]
[ 0.0 0.0 1.0 ]
]
'';
description = ''
Refer to
{manpage}`xrandr(1)`
for the documentation of the transform matrix.
'';
};
dpi = mkOption {
type = types.nullOr types.ints.positive;
description = "Output DPI configuration.";
default = null;
example = 96;
};
scale = mkOption {
type = types.nullOr (types.submodule {
options = {
method = mkOption {
2020-02-02 00:39:17 +01:00
type = types.enum [ "factor" "pixel" ];
description = "Output scaling method.";
default = "factor";
example = "pixel";
};
x = mkOption {
type = types.either types.float types.ints.positive;
description = "Horizontal scaling factor/pixels.";
};
y = mkOption {
type = types.either types.float types.ints.positive;
description = "Vertical scaling factor/pixels.";
};
};
});
description = ''
Output scale configuration.
Either configure by pixels or a scaling factor. When using pixel method the
{manpage}`xrandr(1)`
option
`--scale-from`
will be used; when using factor method the option
`--scale`
will be used.
This option is a shortcut version of the transform option and they are mutually
exclusive.
'';
default = null;
example = literalExpression ''
{
x = 1.25;
y = 1.25;
}
'';
};
filter = mkOption {
type = types.nullOr (types.enum [ "bilinear" "nearest" ]);
description = "Interpolation method to be used for scaling the output.";
default = null;
example = "nearest";
};
2018-04-09 15:04:43 +02:00
};
};
hookType = types.lines;
globalHooksModule = types.submodule {
options = {
postswitch = mkOption {
type = types.attrsOf hookType;
description = "Postswitch hook executed after mode switch.";
2020-02-02 00:39:17 +01:00
default = { };
2018-04-09 15:04:43 +02:00
};
preswitch = mkOption {
type = types.attrsOf hookType;
description = "Preswitch hook executed before mode switch.";
2020-02-02 00:39:17 +01:00
default = { };
2018-04-09 15:04:43 +02:00
};
predetect = mkOption {
type = types.attrsOf hookType;
description = ''
2020-02-02 00:39:17 +01:00
Predetect hook executed before autorandr attempts to run xrandr.
'';
default = { };
2018-04-09 15:04:43 +02:00
};
};
};
profileHooksModule = types.submodule {
options = {
postswitch = mkOption {
type = hookType;
description = "Postswitch hook executed after mode switch.";
2018-04-09 15:04:43 +02:00
default = "";
};
preswitch = mkOption {
type = hookType;
description = "Preswitch hook executed before mode switch.";
2018-04-09 15:04:43 +02:00
default = "";
};
predetect = mkOption {
type = hookType;
description = ''
2020-02-02 00:39:17 +01:00
Predetect hook executed before autorandr attempts to run xrandr.
'';
2018-04-09 15:04:43 +02:00
default = "";
};
};
};
hookToFile = folder: name: hook:
2020-02-02 00:39:17 +01:00
nameValuePair "autorandr/${folder}/${name}" {
source = "${pkgs.writeShellScriptBin "hook" hook}/bin/hook";
};
profileToFiles = name: profile:
with profile;
mkMerge ([
{
"autorandr/${name}/setup".text = concatStringsSep "\n"
(mapAttrsToList fingerprintToString fingerprint);
"autorandr/${name}/config".text =
concatStringsSep "\n" (mapAttrsToList configToString profile.config);
}
(mkIf (hooks.postswitch != "")
(listToAttrs [ (hookToFile name "postswitch" hooks.postswitch) ]))
(mkIf (hooks.preswitch != "")
(listToAttrs [ (hookToFile name "preswitch" hooks.preswitch) ]))
(mkIf (hooks.predetect != "")
(listToAttrs [ (hookToFile name "predetect" hooks.predetect) ]))
]);
2018-04-09 15:04:43 +02:00
fingerprintToString = name: edid: "${name} ${edid}";
2020-02-02 00:39:17 +01:00
configToString = name: config:
if config.enable then
concatStringsSep "\n" ([ "output ${name}" ]
++ optional (config.position != "") "pos ${config.position}"
++ optional (config.crtc != null) "crtc ${toString config.crtc}"
++ optional config.primary "primary"
++ optional (config.dpi != null) "dpi ${toString config.dpi}"
++ optional (config.gamma != "") "gamma ${config.gamma}"
++ optional (config.mode != "") "mode ${config.mode}"
++ optional (config.rate != "") "rate ${config.rate}"
++ optional (config.rotate != null) "rotate ${config.rotate}"
++ optional (config.filter != null) "filter ${config.filter}"
++ optional (config.transform != null) ("transform "
+ concatMapStringsSep "," toString (flatten config.transform))
++ optional (config.scale != null)
((if config.scale.method == "factor" then "scale" else "scale-from")
+ " ${toString config.scale.x}x${toString config.scale.y}"))
else ''
2020-02-02 00:39:17 +01:00
output ${name}
off
'';
in {
2018-04-09 15:04:43 +02:00
options = {
programs.autorandr = {
enable = mkEnableOption "Autorandr";
2018-04-09 15:04:43 +02:00
hooks = mkOption {
type = globalHooksModule;
description = "Global hook scripts";
2020-02-02 00:39:17 +01:00
default = { };
example = literalExpression ''
2020-02-02 00:39:17 +01:00
{
postswitch = {
"notify-i3" = "''${pkgs.i3}/bin/i3-msg restart";
"change-background" = readFile ./change-background.sh;
"change-dpi" = '''
case "$AUTORANDR_CURRENT_PROFILE" in
default)
DPI=120
;;
home)
DPI=192
;;
work)
DPI=144
;;
*)
2022-12-03 05:20:00 +01:00
echo "Unknown profile: $AUTORANDR_CURRENT_PROFILE"
2020-02-02 00:39:17 +01:00
exit 1
esac
echo "Xft.dpi: $DPI" | ''${pkgs.xorg.xrdb}/bin/xrdb -merge
'''
};
}
2018-04-09 15:04:43 +02:00
'';
};
profiles = mkOption {
type = types.attrsOf profileModule;
description = "Autorandr profiles specification.";
2020-02-02 00:39:17 +01:00
default = { };
example = literalExpression ''
2018-04-09 15:04:43 +02:00
{
"work" = {
fingerprint = {
eDP1 = "<EDID>";
DP1 = "<EDID>";
};
config = {
eDP1.enable = false;
DP1 = {
enable = true;
crtc = 0;
2018-04-09 15:04:43 +02:00
primary = true;
position = "0x0";
mode = "3840x2160";
gamma = "1.0:0.909:0.833";
rate = "60.00";
2018-06-25 21:24:36 +02:00
rotate = "left";
2018-04-09 15:04:43 +02:00
};
};
hooks.postswitch = readFile ./work-postswitch.sh;
};
}
'';
};
};
};
config = mkIf cfg.enable {
2020-02-02 00:39:17 +01:00
assertions = flatten (mapAttrsToList (profile:
{ config, ... }:
mapAttrsToList (output: opts: {
assertion = opts.scale == null || opts.transform == null;
message = ''
Cannot use the profile output options 'scale' and 'transform' simultaneously.
Check configuration for: programs.autorandr.profiles.${profile}.config.${output}
'';
}) config) cfg.profiles);
2018-04-09 15:04:43 +02:00
home.packages = [ pkgs.autorandr ];
xdg.configFile = mkMerge ([
(mapAttrs' (hookToFile "postswitch.d") cfg.hooks.postswitch)
2020-02-02 00:39:17 +01:00
(mapAttrs' (hookToFile "preswitch.d") cfg.hooks.preswitch)
(mapAttrs' (hookToFile "predetect.d") cfg.hooks.predetect)
2018-04-09 15:04:43 +02:00
(mkMerge (mapAttrsToList profileToFiles cfg.profiles))
]);
};
meta.maintainers = [ maintainers.uvnikita ];
}