{ config, lib, pkgs, stdenv, ... }: with lib; let cfg = config.services.twmn; animationOpts = { curve = mkOption { type = types.ints.between 0 40; default = 38; example = 19; description = '' The qt easing-curve animation to use for the animation. See QEasingCurve documentation. ''; }; duration = mkOption { type = types.ints.unsigned; default = 1000; example = 618; description = "The animation duration in milliseconds."; }; }; in { meta.maintainers = [ hm.maintainers.austreelis ]; options.services.twmn = { enable = mkEnableOption "twmn, a tiling window manager notification daemon"; duration = mkOption { type = types.ints.unsigned; default = 3000; example = 5000; description = '' The time each notification remains visible, in milliseconds. ''; }; extraConfig = mkOption { type = types.attrs; default = { }; example = literalExpression ''{ main.activation_command = "\${pkgs.hello}/bin/hello"; }''; description = '' Extra configuration options to add to the twmnd config file. See for details. ''; }; host = mkOption { type = types.str; default = "127.0.0.1"; example = "laptop.lan"; description = "Host address to listen on for notifications."; }; icons = { critical = mkOption { type = types.nullOr types.path; default = null; description = "Path to the critical notifications' icon."; }; info = mkOption { type = types.nullOr types.path; default = null; description = "Path to the informative notifications' icon."; }; warning = mkOption { type = types.nullOr types.path; default = null; description = "Path to the warning notifications' icon."; }; }; port = mkOption { type = types.port; default = 9797; description = "UDP port to listen on for notifications."; }; screen = mkOption { type = types.nullOr types.int; default = null; example = 0; description = '' Screen number to display notifications on when using a multi-head desktop. ''; }; soundCommand = mkOption { type = types.str; default = ""; description = "Command to execute to play a notification's sound."; }; text = { color = mkOption { type = types.str; default = "#999999"; example = "lightgray"; description = '' Notification's text color. RGB hex and keywords (e.g. lightgray) are supported. ''; }; font = { package = mkOption { type = types.nullOr types.package; default = null; example = literalExpression "pkgs.dejavu_fonts"; description = '' Notification text's font package. If null then the font is assumed to already be available in your profile. ''; }; family = mkOption { type = types.str; default = "Sans"; example = "Noto Sans"; description = "Notification text's font family."; }; size = mkOption { type = types.ints.unsigned; default = 13; example = 42; description = "Notification text's font size."; }; variant = mkOption { # These are the font variant supported by twmn # See https://github.com/sboli/twmn/blob/master/README.md?plain=1#L42 type = types.enum [ "oblique" "italic" "ultra-light" "light" "medium" "semi-bold" "bold" "ultra-bold" "heavy" "ultra-condensed" "extra-condensed" "condensed" "semi-condensed" "semi-expanded" "expanded" "extra-expanded" "ultra-expanded" ]; default = "medium"; example = "heavy"; description = "Notification text's font variant."; }; }; maxLength = mkOption { type = types.nullOr types.ints.unsigned; default = null; example = 80; description = '' Maximum length of the text before it is cut and suffixed with "...". Never cuts if null. ''; }; }; window = { alwaysOnTop = mkEnableOption "forcing the notification window to always be on top"; animation = { easeIn = mkOption { type = types.submodule { options = animationOpts; }; default = { }; example = literalExpression '' { curve = 19; duration = 618; } ''; description = "Options for the notification appearance's animation."; }; easeOut = mkOption { type = types.submodule { options = animationOpts; }; default = { }; example = literalExpression '' { curve = 19; duration = 618; } ''; description = "Options for the notification disappearance's animation."; }; bounce = { enable = mkEnableOption "notification bounce when displaying next notification directly."; duration = mkOption { type = types.ints.unsigned; default = 500; example = 618; description = "The bounce animation duration in milliseconds."; }; }; }; color = mkOption { type = types.str; default = "#000000"; example = "lightgray"; description = '' Notification's background color. RGB hex and keywords (e.g. lightgray) are supported. ''; }; height = mkOption { type = types.ints.unsigned; default = 18; example = 42; description = '' Height of the slide bar. Useful to match your tiling window manager's bar. ''; }; offset = { x = mkOption { type = types.int; default = 0; example = 50; description = '' Offset of the notification's slide starting point in pixels on the horizontal axis (positive is rightward). ''; }; y = mkOption { type = types.int; default = 0; example = -100; description = '' Offset of the notification's slide starting point in pixels on the vertical axis (positive is upward). ''; }; }; opacity = mkOption { type = types.ints.between 0 100; default = 100; example = 80; description = "The notification window's opacity."; }; position = mkOption { type = types.enum [ "tr" "top_right" "tl" "top_left" "br" "bottom_right" "bl" "bottom_left" "tc" "top_center" "bc" "bottom_center" "c" "center" ]; default = "top_right"; example = "bottom_left"; description = '' Position of the notification slide. The notification will slide in vertically from the border if placed in top_center or bottom_center, horizontally otherwise. ''; }; }; }; ################# # Implementation config = mkIf cfg.enable { assertions = [ (lib.hm.assertions.assertPlatform "services.twmn" pkgs lib.platforms.linux) ]; home.packages = lib.optional (!isNull cfg.text.font.package) cfg.text.font.package ++ [ pkgs.twmn ]; xdg.configFile."twmn/twmn.conf".text = let conf = recursiveUpdate { gui = { always_on_top = if cfg.window.alwaysOnTop then "true" else "false"; background_color = cfg.window.color; bounce = if cfg.window.animation.bounce.enable then "true" else "false"; bounce_duration = toString cfg.window.animation.bounce.duration; font = cfg.text.font.family; font_size = toString cfg.text.font.size; font_variant = cfg.text.font.variant; foreground_color = cfg.text.color; height = toString cfg.window.height; in_animation = toString cfg.window.animation.easeIn.curve; in_animation_duration = toString cfg.window.animation.easeIn.duration; max_length = toString (if isNull cfg.text.maxLength then -1 else cfg.text.maxLength); offset_x = with cfg.window.offset; if x < 0 then toString x else "+${toString x}"; offset_y = with cfg.window.offset; if y < 0 then toString y else "+${toString y}"; opacity = toString cfg.window.opacity; out_animation = toString cfg.window.animation.easeOut.curve; out_animation_duration = toString cfg.window.animation.easeOut.duration; position = cfg.window.position; screen = toString cfg.screen; }; # map null values to empty strings because formats.toml generator fails # when encountering a null. icons = mapAttrs (_: toString) cfg.icons; main = { duration = toString cfg.duration; host = cfg.host; port = toString cfg.port; sound_command = cfg.soundCommand; }; } cfg.extraConfig; mkLine = name: value: "${name}=${value}"; mkSection = section: conf: '' [${section}] ${concatStringsSep "\n" (mapAttrsToList mkLine conf)} ''; in concatStringsSep "\n" (mapAttrsToList mkSection conf) + "\n"; systemd.user.services.twmnd = { Unit = { Description = "twmn daemon"; After = [ "graphical-session-pre.target" ]; PartOf = [ "graphical-session.target" ]; X-Restart-Triggers = [ "${config.xdg.configFile."twmn/twmn.conf".source}" ]; }; Install.WantedBy = [ "graphical-session.target" ]; Service = { ExecStart = "${pkgs.twmn}/bin/twmnd"; Restart = "on-failure"; Type = "simple"; StandardOutput = "null"; }; }; }; }