1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-11-08 11:36:05 +01:00
home-manager/modules/services/kanshi.nix
Austin Horstman 86402a17b6 treewide: flatten single file modules
Some files don't need nesting and can be root level again to reduce
conflicts with other PRs.

Signed-off-by: Austin Horstman <khaneliman12@gmail.com>
2025-06-23 16:20:26 -05:00

381 lines
9.3 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
concatStringsSep
literalExpression
mkIf
mkOption
optionalString
types
;
cfg = config.services.kanshi;
directivesTag = types.attrTag {
profile = mkOption {
type = profileModule;
description = ''
profile attribute set.
'';
};
output = mkOption {
type = outputModule;
description = ''
output attribute set.
'';
};
include = mkOption {
type = types.str;
description = ''
Include as another file from _path_.
Expands shell syntax (see *wordexp*(3) for details).
'';
};
};
tagToStr =
x:
if x ? profile then
profileStr x.profile
else if x ? output then
outputStr x.output
else if x ? include then
''include "${x.include}"''
else
throw "Unknown tags ${lib.attrNames x}";
directivesStr = concatStringsSep "\n" (map tagToStr cfg.settings);
oldDirectivesStr = ''
${concatStringsSep "\n" (lib.mapAttrsToList (n: v: profileStr (v // { name = n; })) cfg.profiles)}
${cfg.extraConfig}
'';
outputModule = types.submodule {
options = {
criteria = mkOption {
type = types.str;
description = ''
The criteria can either be an output name, an output description or "*".
The latter can be used to match any output.
On
{manpage}`sway(1)`,
output names and descriptions can be obtained via
`swaymsg -t get_outputs`.
'';
};
status = mkOption {
type = types.nullOr (
types.enum [
"enable"
"disable"
]
);
default = null;
description = ''
Enables or disables the specified output.
'';
};
mode = mkOption {
type = types.nullOr types.str;
default = null;
example = "1920x1080@60Hz";
description = ''
&lt;width&gt;x&lt;height&gt;[@&lt;rate&gt;[Hz]]
Configures the specified output to use the specified mode.
Modes are a combination of width and height (in pixels) and
a refresh rate (in Hz) that your display can be configured to use.
'';
};
position = mkOption {
type = types.nullOr types.str;
default = null;
example = "1600,0";
description = ''
&lt;x&gt;,&lt;y&gt;
Places the output at the specified position in the global coordinates
space.
'';
};
scale = mkOption {
type = types.nullOr types.float;
default = null;
example = 2;
description = ''
Scales the output by the specified scale factor.
'';
};
transform = mkOption {
type = types.nullOr (
types.enum [
"normal"
"90"
"180"
"270"
"flipped"
"flipped-90"
"flipped-180"
"flipped-270"
]
);
default = null;
description = ''
Sets the output transform.
'';
};
alias = mkOption {
type = types.nullOr types.str;
default = null;
example = "laptopMonitor";
description = ''
Defines an alias for the output
'';
};
adaptiveSync = mkOption {
type = types.nullOr types.bool;
default = null;
example = true;
description = ''
Enables or disables adaptive synchronization
(aka. Variable Refresh Rate).
'';
};
};
};
outputStr =
{
criteria,
status,
mode,
position,
scale,
transform,
adaptiveSync,
alias,
...
}:
''output "${criteria}"''
+ optionalString (status != null) " ${status}"
+ optionalString (mode != null) " mode ${mode}"
+ optionalString (position != null) " position ${position}"
+ optionalString (scale != null) " scale ${toString scale}"
+ optionalString (transform != null) " transform ${transform}"
+ optionalString (adaptiveSync != null) " adaptive_sync ${if adaptiveSync then "on" else "off"}"
+ optionalString (alias != null) " alias \$${alias}";
profileModule = types.submodule {
options = {
outputs = mkOption {
type = types.listOf outputModule;
default = [ ];
description = ''
Outputs configuration.
'';
};
name = mkOption {
type = types.str;
default = "";
description = ''
Profile name
'';
};
exec = mkOption {
type = with types; coercedTo str lib.singleton (listOf str);
default = [ ];
example = "[ \${pkg.sway}/bin/swaymsg workspace 1, move workspace to eDP-1 ]";
description = ''
Commands executed after the profile is successfully applied.
Note that if you provide multiple commands, they will be
executed asynchronously with no guaranteed ordering.
'';
};
};
};
profileStr =
{ outputs, exec, ... }@args:
''
profile ${args.name or ""} {
${concatStringsSep "\n " (map outputStr outputs ++ map (cmd: "exec ${cmd}") exec)}
}
'';
in
{
meta.maintainers = [ lib.hm.maintainers.nurelin ];
options.services.kanshi = {
enable = lib.mkEnableOption "kanshi, a Wayland daemon that automatically configures outputs";
package = lib.mkPackageOption pkgs "kanshi" { };
profiles = mkOption {
type = types.attrsOf profileModule;
default = { };
description = ''
Attribute set of profiles.
'';
example = literalExpression ''
{
undocked = {
outputs = [
{
criteria = "eDP-1";
}
];
};
docked = {
outputs = [
{
criteria = "eDP-1";
}
{
criteria = "Some Company ASDF 4242";
transform = "90";
}
];
};
}
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
Extra configuration lines to append to the kanshi
configuration file.
'';
};
settings = mkOption {
type = types.listOf directivesTag;
default = [ ];
description = ''
Ordered list of directives.
See kanshi(5) for informations.
'';
example = literalExpression ''
[
{ include = "path/to/included/files"; }
{ output.criteria = "eDP-1";
output.scale = 2;
}
{ profile.name = "undocked";
profile.outputs = [
{
criteria = "eDP-1";
}
];
}
{ profile.name = "docked";
profile.outputs = [
{
criteria = "eDP-1";
}
{
criteria = "Some Company ASDF 4242";
transform = "90";
}
];
}
]
'';
};
systemdTarget = mkOption {
type = types.str;
default = config.wayland.systemd.target;
defaultText = literalExpression "config.wayland.systemd.target";
description = ''
Systemd target to bind to.
'';
};
};
config = mkIf cfg.enable (
lib.mkMerge [
{
assertions = [
(lib.hm.assertions.assertPlatform "services.kanshi" pkgs lib.platforms.linux)
{
assertion = (cfg.profiles == { } && cfg.extraConfig == "") || (lib.length cfg.settings) == 0;
message = "Cannot mix kanshi.settings with kanshi.profiles or kanshi.extraConfig";
}
{
assertion =
let
profiles = lib.filter (x: x ? profile) cfg.settings;
in
lib.length (lib.filter (x: lib.any (a: a ? alias && a.alias != null) x.profile.outputs) profiles)
== 0;
message = "Output kanshi.*.output.alias can only be defined on global scope";
}
];
}
(mkIf (cfg.profiles != { }) {
warnings = [
"kanshi.profiles option is deprecated. Use kanshi.settings instead."
];
})
(mkIf (cfg.extraConfig != "") {
warnings = [
"kanshi.extraConfig option is deprecated. Use kanshi.settings instead."
];
})
{
home.packages = [ cfg.package ];
xdg.configFile."kanshi/config" =
let
generatedConfigStr =
if cfg.profiles == { } && cfg.extraConfig == "" then directivesStr else oldDirectivesStr;
in
mkIf (generatedConfigStr != "") { text = generatedConfigStr; };
systemd.user.services.kanshi = {
Unit = {
Description = "Dynamic output configuration";
Documentation = "man:kanshi(1)";
ConditionEnvironment = "WAYLAND_DISPLAY";
PartOf = cfg.systemdTarget;
Requires = cfg.systemdTarget;
After = cfg.systemdTarget;
};
Service = {
Type = "simple";
ExecStart = "${cfg.package}/bin/kanshi";
Restart = "always";
};
Install = {
WantedBy = [ cfg.systemdTarget ];
};
};
}
]
);
}