From 63dccc4e60422c1db2c3929b2fd1541f36b7e664 Mon Sep 17 00:00:00 2001 From: Morgane Austreelis Date: Sun, 24 Oct 2021 15:50:13 +0200 Subject: [PATCH] twmn: add module This module allows to configure and start the twmn daemon. --- .github/CODEOWNERS | 3 + modules/lib/maintainers.nix | 6 + modules/misc/news.nix | 8 + modules/modules.nix | 1 + modules/services/twmn.nix | 380 ++++++++++++++++++ tests/default.nix | 1 + .../services/twmn/basic-configuration.conf | 32 ++ .../services/twmn/basic-configuration.nix | 51 +++ tests/modules/services/twmn/default.nix | 1 + 9 files changed, 483 insertions(+) create mode 100644 modules/services/twmn.nix create mode 100644 tests/modules/services/twmn/basic-configuration.conf create mode 100644 tests/modules/services/twmn/basic-configuration.nix create mode 100644 tests/modules/services/twmn/default.nix diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 73f190829..8a2f8a671 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -370,6 +370,9 @@ /modules/services/trayer.nix @AndreasMager /tests/modules/services/trayer @AndreasMager +/modules/services/twmn.nix @Austreelis +/tests/modules/services/twmn @Austreelis + /modules/services/udiskie.nix @rycee /modules/services/unison.nix @pacien diff --git a/modules/lib/maintainers.nix b/modules/lib/maintainers.nix index f912bbd5f..13bdb1e0f 100644 --- a/modules/lib/maintainers.nix +++ b/modules/lib/maintainers.nix @@ -13,6 +13,12 @@ github = "amesgen"; githubId = 15369874; }; + austreelis = { + email = "github@accounts.austreelis.net"; + github = "Austreelis"; + githubId = 56743515; + name = "Morgane Austreelis"; + }; CarlosLoboxyz = { name = "Carlos Lobo"; email = "86011416+CarlosLoboxyz@users.noreply.github.com"; diff --git a/modules/misc/news.nix b/modules/misc/news.nix index b5678546f..41fe07cee 100644 --- a/modules/misc/news.nix +++ b/modules/misc/news.nix @@ -2395,6 +2395,14 @@ in A new module is available: 'programs.kodi'. ''; } + + { + time = "2022-02-03T23:23:49+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'services.twmn'. + ''; + } ]; }; } diff --git a/modules/modules.nix b/modules/modules.nix index b52a7aac8..528734dc6 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -236,6 +236,7 @@ let ./services/tahoe-lafs.nix ./services/taskwarrior-sync.nix ./services/trayer.nix + ./services/twmn.nix ./services/udiskie.nix ./services/unclutter.nix ./services/unison.nix diff --git a/modules/services/twmn.nix b/modules/services/twmn.nix new file mode 100644 index 000000000..d4c126829 --- /dev/null +++ b/modules/services/twmn.nix @@ -0,0 +1,380 @@ +{ 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"; + }; + }; + }; +} diff --git a/tests/default.nix b/tests/default.nix index da6644f59..73e99639b 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -156,6 +156,7 @@ import nmt { ./modules/services/sxhkd ./modules/services/syncthing ./modules/services/trayer + ./modules/services/twmn ./modules/services/window-managers/bspwm ./modules/services/window-managers/herbstluftwm ./modules/services/window-managers/i3 diff --git a/tests/modules/services/twmn/basic-configuration.conf b/tests/modules/services/twmn/basic-configuration.conf new file mode 100644 index 000000000..a6ddb5c73 --- /dev/null +++ b/tests/modules/services/twmn/basic-configuration.conf @@ -0,0 +1,32 @@ +[gui] +always_on_top=true +background_color=black +bounce=true +bounce_duration=271 +font=Noto Sans +font_size=16 +font_variant=italic +foreground_color=#FF00FF +height=20 +in_animation=27 +in_animation_duration=314 +max_length=80 +offset_x=+20 +offset_y=-60 +opacity=80 +out_animation=13 +out_animation_duration=168 +position=center +screen=0 + +[icons] +critical=/path/icon/critical +info=/path/icon/info +warning=/path/icon/warning + +[main] +duration=4242 +host=example.com +port=9006 +sound_command=/path/sound/command + diff --git a/tests/modules/services/twmn/basic-configuration.nix b/tests/modules/services/twmn/basic-configuration.nix new file mode 100644 index 000000000..5da926e48 --- /dev/null +++ b/tests/modules/services/twmn/basic-configuration.nix @@ -0,0 +1,51 @@ +{ + config = { + services.twmn = { + enable = true; + duration = 4242; + host = "example.com"; + port = 9006; + screen = 0; + soundCommand = "/path/sound/command"; + icons.critical = "/path/icon/critical"; + icons.info = "/path/icon/info"; + icons.warning = "/path/icon/warning"; + text = { + color = "#FF00FF"; + font.family = "Noto Sans"; + font.size = 16; + font.variant = "italic"; + maxLength = 80; + }; + window = { + alwaysOnTop = true; + color = "black"; + height = 20; + offset.x = 20; + offset.y = -60; + opacity = 80; + position = "center"; + animation = { + easeIn.curve = 27; + easeIn.duration = 314; + easeOut.curve = 13; + easeOut.duration = 168; + bounce.enable = true; + bounce.duration = 271; + }; + }; + }; + + test.stubs.twmn = { }; + + nmt.script = '' + serviceFile="home-files/.config/systemd/user/twmnd.service" + assertFileExists "$serviceFile" + assertFileRegex "$serviceFile" 'X-Restart-Triggers=.*twmn\.conf' + assertFileRegex "$serviceFile" 'ExecStart=@twmn@/bin/twmnd' + assertFileExists "home-files/.config/twmn/twmn.conf" + assertFileContent "home-files/.config/twmn/twmn.conf" \ + ${./basic-configuration.conf} + ''; + }; +} diff --git a/tests/modules/services/twmn/default.nix b/tests/modules/services/twmn/default.nix new file mode 100644 index 000000000..8a021ba38 --- /dev/null +++ b/tests/modules/services/twmn/default.nix @@ -0,0 +1 @@ +{ twmn-basic-configuration = ./basic-configuration.nix; }