From f4f9f1a618eb7c19b765b02c235e13657b5aa150 Mon Sep 17 00:00:00 2001 From: Nicolas Berbiche Date: Wed, 29 Jul 2020 16:29:51 -0400 Subject: [PATCH] waybar: add module PR #1329 --- .github/CODEOWNERS | 3 + modules/lib/maintainers.nix | 10 + modules/misc/news.nix | 8 + modules/modules.nix | 1 + modules/programs/waybar.nix | 363 ++++++++++++++++++ tests/default.nix | 1 + .../programs/waybar/broken-settings.nix | 80 ++++ tests/modules/programs/waybar/default.nix | 8 + .../waybar/settings-complex-expected.json | 46 +++ .../programs/waybar/settings-complex.nix | 59 +++ .../programs/waybar/styling-expected.css | 23 ++ tests/modules/programs/waybar/styling.nix | 46 +++ .../systemd-with-graphical-session-target.nix | 24 ++ ...temd-with-graphical-session-target.service | 16 + 14 files changed, 688 insertions(+) create mode 100644 modules/programs/waybar.nix create mode 100644 tests/modules/programs/waybar/broken-settings.nix create mode 100644 tests/modules/programs/waybar/default.nix create mode 100644 tests/modules/programs/waybar/settings-complex-expected.json create mode 100644 tests/modules/programs/waybar/settings-complex.nix create mode 100644 tests/modules/programs/waybar/styling-expected.css create mode 100644 tests/modules/programs/waybar/styling.nix create mode 100644 tests/modules/programs/waybar/systemd-with-graphical-session-target.nix create mode 100644 tests/modules/programs/waybar/systemd-with-graphical-session-target.service diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ee1781ea2..3f37aec0d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -103,6 +103,9 @@ /modules/programs/texlive.nix @rycee +/modules/programs/waybar.nix @berbiche +/tests/modules/programs/waybar @berbiche + /modules/programs/z-lua.nix @marsam /modules/programs/zathura.nix @rprospero diff --git a/modules/lib/maintainers.nix b/modules/lib/maintainers.nix index 49d14ef06..ff0463600 100644 --- a/modules/lib/maintainers.nix +++ b/modules/lib/maintainers.nix @@ -25,4 +25,14 @@ github = "cwyc"; githubId = 16950437; }; + berbiche = { + name = "Nicolas Berbiche"; + email = "berbiche@users.noreply.github.com"; + github = "berbiche"; + githubId = 20448408; + keys = [{ + longkeyid = "rsa4096/0xB461292445C6E696"; + fingerprint = "D446 E58D 87A0 31C7 EC15 88D7 B461 2924 45C6 E696"; + }]; + }; } diff --git a/modules/misc/news.nix b/modules/misc/news.nix index 62f57a44c..4ec6fc2d6 100644 --- a/modules/misc/news.nix +++ b/modules/misc/news.nix @@ -1619,6 +1619,14 @@ in A new module is available: 'services.dropbox'. ''; } + + { + time = "2020-08-13T22:15:27+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'programs.waybar' + ''; + } ]; }; } diff --git a/modules/modules.nix b/modules/modules.nix index 76ff5aaa8..41877c5e2 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -118,6 +118,7 @@ let (loadModule ./programs/vim.nix { }) (loadModule ./programs/vscode.nix { }) (loadModule ./programs/vscode/haskell.nix { }) + (loadModule ./programs/waybar.nix { condition = hostPlatform.isLinux; }) (loadModule ./programs/z-lua.nix { }) (loadModule ./programs/zathura.nix { }) (loadModule ./programs/zoxide.nix { }) diff --git a/modules/programs/waybar.nix b/modules/programs/waybar.nix new file mode 100644 index 000000000..369f6e32a --- /dev/null +++ b/modules/programs/waybar.nix @@ -0,0 +1,363 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.programs.waybar; + + # Used when generating warnings + modulesPath = "programs.waybar.settings.[].modules"; + + # Taken from + defaultModuleNames = [ + "sway/mode" + "sway/workspaces" + "sway/window" + "wlr/taskbar" + "idle_inhibitor" + "memory" + "cpu" + "clock" + "disk" + "tray" + "network" + "backlight" + "pulseaudio" + "mpd" + "temperature" + "bluetooth" + "battery" + ]; + + isValidCustomModuleName = x: + elem x defaultModuleNames || (hasPrefix "custom/" x && stringLength x > 7); + + margins = let + mkMargin = name: { + "margin-${name}" = mkOption { + type = types.nullOr types.int; + default = null; + example = 10; + description = "Margins value without unit."; + }; + }; + margins = map mkMargin [ "top" "left" "bottom" "right" ]; + in foldl' mergeAttrs { } margins; + + waybarBarConfig = with lib.types; + submodule { + options = { + layer = mkOption { + type = nullOr (enum [ "top" "bottom" ]); + default = null; + description = '' + Decide if the bar is displayed in front ("top") + of the windows or behind ("bottom"). + ''; + example = "top"; + }; + + output = mkOption { + type = nullOr (either str (listOf str)); + default = null; + example = literalExample '' + [ "DP-1" "!DP-2" "!DP-3" ] + ''; + description = '' + Specifies on which screen this bar will be displayed. + Exclamation mark(!) can be used to exclude specific output. + ''; + }; + + position = mkOption { + type = nullOr (enum [ "top" "bottom" "left" "right" ]); + default = null; + example = "right"; + description = "Bar position relative to the output."; + }; + + height = mkOption { + type = nullOr ints.unsigned; + default = null; + example = 5; + description = + "Height to be used by the bar if possible. Leave blank for a dynamic value."; + }; + + width = mkOption { + type = nullOr ints.unsigned; + default = null; + example = 5; + description = + "Width to be used by the bar if possible. Leave blank for a dynamic value."; + }; + + modules-left = mkOption { + type = nullOr (listOf str); + default = null; + description = "Modules that will be displayed on the left."; + example = literalExample '' + [ "sway/workspaces" "sway/mode" "wlr/taskbar" ] + ''; + }; + + modules-center = mkOption { + type = nullOr (listOf str); + default = null; + description = "Modules that will be displayed in the center."; + example = literalExample '' + [ "sway/window" ] + ''; + }; + + modules-right = mkOption { + type = nullOr (listOf str); + default = null; + description = "Modules that will be displayed on the right."; + example = literalExample '' + [ "mpd" "custom/mymodule#with-css-id" "temperature" ] + ''; + }; + + modules = mkOption { + type = attrsOf unspecified; + default = { }; + description = "Modules configuration."; + example = literalExample '' + { + "sway/window" = { + max-length = 50; + }; + "clock" = { + format-alt = "{:%a, %d. %b %H:%M}"; + }; + } + ''; + }; + + margin = mkOption { + type = nullOr str; + default = null; + description = "Margins value using the CSS format without units."; + example = "20 5"; + }; + + inherit (margins) margin-top margin-left margin-bottom margin-right; + + name = mkOption { + type = nullOr str; + default = null; + description = + "Optional name added as a CSS class, for styling multiple waybars."; + example = "waybar-1"; + }; + + gtk-layer-shell = mkOption { + type = nullOr bool; + default = null; + example = false; + description = + "Option to disable the use of gtk-layer-shell for popups."; + }; + }; + }; +in { + meta.maintainers = [ hm.maintainers.berbiche ]; + + options.programs.waybar = with lib.types; { + enable = mkEnableOption "Waybar"; + + package = mkOption { + type = package; + default = pkgs.waybar; + defaultText = literalExample "${pkgs.waybar}"; + description = '' + Waybar package to use. Set to null to use the default module. + ''; + }; + + settings = mkOption { + type = listOf waybarBarConfig; + default = [ ]; + description = '' + Configuration for Waybar, see + for supported values. + ''; + example = literalExample '' + [ + { + layer = "top"; + position = "top"; + height = 30; + output = [ + "eDP-1" + "HDMI-A-1" + ]; + modules-left = [ "sway/workspaces" "sway/mode" "wlr/taskbar" ]; + modules-center = [ "sway/window" "custom/hello-from-waybar" ]; + modules-right = [ "mpd" "custom/mymodule#with-css-id" "temperature" ]; + modules = { + "sway/workspaces" = { + disable-scroll = true; + all-outputs = true; + }; + "custom/hello-from-waybar" = { + format = "hello {}"; + max-length = 40; + interval = "once"; + exec = pkgs.writeShellScript "hello-from-waybar" ''' + echo "from within waybar" + '''; + }; + }; + } + ] + ''; + }; + + systemd.enable = mkEnableOption "Waybar systemd integration"; + + style = mkOption { + type = nullOr str; + default = null; + description = '' + CSS style of the bar. + See + for the documentation. + ''; + example = '' + * { + border: none; + border-radius: 0; + font-family: Source Code Pro; + } + window#waybar { + background: #16191C; + color: #AAB2BF; + } + #workspaces button { + padding: 0 5px; + } + ''; + }; + }; + + config = let + # Inspired by https://github.com/NixOS/nixpkgs/pull/89781 + writePrettyJSON = name: x: + pkgs.runCommandLocal name { } '' + ${pkgs.jq}/bin/jq . > $out <<<${escapeShellArg (builtins.toJSON x)} + ''; + + configSource = let + # Removes nulls because Waybar ignores them for most values + removeNulls = filterAttrs (_: v: v != null); + + # Makes the actual valid configuration Waybar accepts + # (strips our custom settings before converting to JSON) + makeConfiguration = configuration: + let + # The "modules" option is not valid in the JSON + # as its descendants have to live at the top-level + settingsWithoutModules = + filterAttrs (n: _: n != "modules") configuration; + settingsModules = + optionalAttrs (configuration.modules != { }) configuration.modules; + in removeNulls (settingsWithoutModules // settingsModules); + # The clean list of configurations + finalConfiguration = map makeConfiguration cfg.settings; + in writePrettyJSON "waybar-config.json" finalConfiguration; + + warnings = let + mkUnreferencedModuleWarning = name: + "The module '${name}' defined in '${modulesPath}' is not referenced " + + "in either `modules-left`, `modules-center` or `modules-right` of Waybar's options"; + mkUndefinedModuleWarning = settings: name: + let + # Locations where the module is undefined (a combination modules-{left,center,right}) + locations = flip filter [ "left" "center" "right" ] + (x: elem name settings."modules-${x}"); + mkPath = loc: "'${modulesPath}-${loc}'"; + # The modules-{left,center,right} configuration that includes + # an undefined module + path = concatMapStringsSep " and " mkPath locations; + in "The module '${name}' defined in ${path} is neither " + + "a default module or a custom module declared in '${modulesPath}'"; + mkInvalidModuleNameWarning = name: + "The custom module '${name}' defined in '${modulesPath}' is not a valid " + + "module name. A custom module's name must start with 'custom/' " + + "like 'custom/mymodule' for instance"; + + # Find all modules in `modules-{left,center,right}` and `modules` not declared/referenced. + # `cfg.settings` is a list of Waybar configurations + # and we need to preserve the index for appropriate warnings + allFaultyModules = flip map cfg.settings (settings: + let + allModules = unique + (concatMap (x: attrByPath [ "modules-${x}" ] [ ] settings) [ + "left" + "center" + "right" + ]); + declaredModules = attrNames settings.modules; + # Modules declared in `modules` but not referenced in `modules-{left,center,right}` + unreferencedModules = subtractLists allModules declaredModules; + # Modules listed in modules-{left,center,right} that are not default modules + nonDefaultModules = subtractLists defaultModuleNames allModules; + # Modules referenced in `modules-{left,center,right}` but not declared in `modules` + undefinedModules = subtractLists declaredModules nonDefaultModules; + # Check for invalid module names + invalidModuleNames = + filter (m: !isValidCustomModuleName m) (attrNames settings.modules); + in { + # The Waybar bar configuration (since config.settings is a list) + settings = settings; + undef = undefinedModules; + unref = unreferencedModules; + invalidName = invalidModuleNames; + }); + + allWarnings = flip concatMap allFaultyModules + ({ settings, undef, unref, invalidName }: + let + unreferenced = map mkUnreferencedModuleWarning unref; + undefined = map (mkUndefinedModuleWarning settings) undef; + invalid = map mkInvalidModuleNameWarning invalidName; + in undefined ++ unreferenced ++ invalid); + in allWarnings; + + in mkIf cfg.enable (mkMerge [ + { home.packages = [ cfg.package ]; } + (mkIf (cfg.settings != [ ]) { + # Generate warnings about defined but unreferenced modules + inherit warnings; + + xdg.configFile."waybar/config".source = configSource; + }) + (mkIf (cfg.style != null) { + xdg.configFile."waybar/style.css".text = cfg.style; + }) + (mkIf cfg.systemd.enable { + systemd.user.services.waybar = { + Unit = { + Description = + "Highly customizable Wayland bar for Sway and Wlroots based compositors."; + Documentation = "https://github.com/Alexays/Waybar/wiki"; + PartOf = [ "graphical-session.target" ]; + Requisite = [ "dbus.service" ]; + After = [ "dbus.service" ]; + }; + + Service = { + Type = "dbus"; + BusName = "fr.arouillard.waybar"; + ExecStart = "${cfg.package}/bin/waybar"; + Restart = "always"; + RestartSec = "1sec"; + }; + + Install = { WantedBy = [ "graphical-session.target" ]; }; + }; + }) + ]); +} diff --git a/tests/default.nix b/tests/default.nix index 53c221bcf..939a50e2d 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -82,6 +82,7 @@ import nmt { ./modules/programs/getmail ./modules/services/lieer ./modules/programs/rofi + ./modules/programs/waybar ./modules/services/polybar ./modules/services/sxhkd ./modules/services/fluidsynth diff --git a/tests/modules/programs/waybar/broken-settings.nix b/tests/modules/programs/waybar/broken-settings.nix new file mode 100644 index 000000000..68f0b90bf --- /dev/null +++ b/tests/modules/programs/waybar/broken-settings.nix @@ -0,0 +1,80 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + package = pkgs.writeScriptBin "dummy-waybar" "" // { outPath = "@waybar@"; }; + expected = pkgs.writeText "expected-json" '' + [ + { + "height": 26, + "layer": "top", + "modules-center": [ + "sway/window" + ], + "modules-left": [ + "sway/workspaces", + "sway/mode" + ], + "modules-right": [ + "idle_inhibitor", + "pulseaudio", + "network", + "cpu", + "memory", + "backlight", + "tray", + "clock" + ], + "output": [ + "DP-1", + "eDP-1", + "HEADLESS-1" + ], + "position": "top", + "sway/workspaces": { + "all-outputs": true + } + } + ] + ''; +in { + config = { + programs.waybar = { + inherit package; + enable = true; + systemd.enable = true; + settings = [{ + layer = "top"; + position = "top"; + height = 26; + output = [ "DP-1" "eDP-1" "HEADLESS-1" ]; + modules-left = [ "sway/workspaces" "sway/mode" ]; + modules-center = [ "sway/window" ]; + modules-right = [ + "idle_inhibitor" + "pulseaudio" + "network" + "cpu" + "memory" + "backlight" + "tray" + "clock" + ]; + + modules = { "sway/workspaces".all-outputs = true; }; + }]; + }; + + nmt.description = '' + Test for the broken configuration + https://github.com/rycee/home-manager/pull/1329#issuecomment-653253069 + ''; + nmt.script = '' + assertPathNotExists home-files/.config/waybar/style.css + assertFileContent \ + home-files/.config/waybar/config \ + ${expected} + ''; + }; +} diff --git a/tests/modules/programs/waybar/default.nix b/tests/modules/programs/waybar/default.nix new file mode 100644 index 000000000..d50b1b8d3 --- /dev/null +++ b/tests/modules/programs/waybar/default.nix @@ -0,0 +1,8 @@ +{ + waybar-systemd-with-graphical-session-target = + ./systemd-with-graphical-session-target.nix; + waybar-styling = ./styling.nix; + waybar-settings-complex = ./settings-complex.nix; + # Broken configuration from https://github.com/rycee/home-manager/pull/1329#issuecomment-653253069 + waybar-broken-settings = ./broken-settings.nix; +} diff --git a/tests/modules/programs/waybar/settings-complex-expected.json b/tests/modules/programs/waybar/settings-complex-expected.json new file mode 100644 index 000000000..0d020c19c --- /dev/null +++ b/tests/modules/programs/waybar/settings-complex-expected.json @@ -0,0 +1,46 @@ +[ + { + "custom/my-module": { + "exec": "@dummy@/bin/dummy", + "format": "hello from {}" + }, + "height": 30, + "idle_inhibitor": { + "format": "{icon}" + }, + "layer": "top", + "modules-center": [ + "sway/window" + ], + "modules-left": [ + "sway/workspaces", + "sway/mode", + "custom/my-module" + ], + "modules-right": [ + "idle_inhibitor", + "pulseaudio", + "network", + "cpu", + "memory", + "backlight", + "tray", + "battery", + "clock" + ], + "output": [ + "DP-1" + ], + "position": "top", + "sway/mode": { + "tooltip": false + }, + "sway/window": { + "max-length": 120 + }, + "sway/workspaces": { + "all-outputs": true, + "disable-scroll": true + } + } +] diff --git a/tests/modules/programs/waybar/settings-complex.nix b/tests/modules/programs/waybar/settings-complex.nix new file mode 100644 index 000000000..750e52f45 --- /dev/null +++ b/tests/modules/programs/waybar/settings-complex.nix @@ -0,0 +1,59 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + package = pkgs.writeScriptBin "dummy-waybar" "" // { outPath = "@waybar@"; }; +in { + config = { + programs.waybar = { + inherit package; + enable = true; + settings = [{ + layer = "top"; + position = "top"; + height = 30; + output = [ "DP-1" ]; + modules-left = [ "sway/workspaces" "sway/mode" "custom/my-module" ]; + modules-center = [ "sway/window" ]; + modules-right = [ + "idle_inhibitor" + "pulseaudio" + "network" + "cpu" + "memory" + "backlight" + "tray" + "battery" + "clock" + ]; + + modules = { + "sway/workspaces" = { + disable-scroll = true; + all-outputs = true; + }; + "sway/mode" = { tooltip = false; }; + "sway/window" = { max-length = 120; }; + "idle_inhibitor" = { format = "{icon}"; }; + "custom/my-module" = { + format = "hello from {}"; + exec = let + dummyScript = + pkgs.writeShellScriptBin "dummy" "echo within waybar" // { + outPath = "@dummy@"; + }; + in "${dummyScript}/bin/dummy"; + }; + }; + }]; + }; + + nmt.script = '' + assertPathNotExists home-files/.config/waybar/style.css + assertFileContent \ + home-files/.config/waybar/config \ + ${./settings-complex-expected.json} + ''; + }; +} diff --git a/tests/modules/programs/waybar/styling-expected.css b/tests/modules/programs/waybar/styling-expected.css new file mode 100644 index 000000000..dc779e587 --- /dev/null +++ b/tests/modules/programs/waybar/styling-expected.css @@ -0,0 +1,23 @@ +* { + border: none; + border-radius: 0; + font-family: Source Code Pro; + font-weight: bold; + color: #abb2bf; + font-size: 18px; + min-height: 0px; +} +window#waybar { + background: #16191C; + color: #aab2bf; +} +#window { + padding: 0 0px; +} +#workspaces button:hover { + box-shadow: inherit; + text-shadow: inherit; + background: #16191C; + border: #16191C; + padding: 0 3px; +} diff --git a/tests/modules/programs/waybar/styling.nix b/tests/modules/programs/waybar/styling.nix new file mode 100644 index 000000000..bd73f2aaf --- /dev/null +++ b/tests/modules/programs/waybar/styling.nix @@ -0,0 +1,46 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + package = pkgs.writeScriptBin "dummy-waybar" "" // { outPath = "@waybar@"; }; +in { + config = { + programs.waybar = { + inherit package; + enable = true; + style = '' + * { + border: none; + border-radius: 0; + font-family: Source Code Pro; + font-weight: bold; + color: #abb2bf; + font-size: 18px; + min-height: 0px; + } + window#waybar { + background: #16191C; + color: #aab2bf; + } + #window { + padding: 0 0px; + } + #workspaces button:hover { + box-shadow: inherit; + text-shadow: inherit; + background: #16191C; + border: #16191C; + padding: 0 3px; + } + ''; + }; + + nmt.script = '' + assertPathNotExists home-files/.config/waybar/config + assertFileContent \ + home-files/.config/waybar/style.css \ + ${./styling-expected.css} + ''; + }; +} diff --git a/tests/modules/programs/waybar/systemd-with-graphical-session-target.nix b/tests/modules/programs/waybar/systemd-with-graphical-session-target.nix new file mode 100644 index 000000000..e751d804d --- /dev/null +++ b/tests/modules/programs/waybar/systemd-with-graphical-session-target.nix @@ -0,0 +1,24 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + package = pkgs.writeScriptBin "dummy-waybar" "" // { outPath = "@waybar@"; }; +in { + config = { + programs.waybar = { + inherit package; + enable = true; + systemd.enable = true; + }; + + nmt.script = '' + assertPathNotExists home-files/.config/waybar/config + assertPathNotExists home-files/.config/waybar/style.css + + assertFileContent \ + home-files/.config/systemd/user/waybar.service \ + ${./systemd-with-graphical-session-target.service} + ''; + }; +} diff --git a/tests/modules/programs/waybar/systemd-with-graphical-session-target.service b/tests/modules/programs/waybar/systemd-with-graphical-session-target.service new file mode 100644 index 000000000..7d4c65214 --- /dev/null +++ b/tests/modules/programs/waybar/systemd-with-graphical-session-target.service @@ -0,0 +1,16 @@ +[Install] +WantedBy=graphical-session.target + +[Service] +BusName=fr.arouillard.waybar +ExecStart=@waybar@/bin/waybar +Restart=always +RestartSec=1sec +Type=dbus + +[Unit] +After=dbus.service +Description=Highly customizable Wayland bar for Sway and Wlroots based compositors. +Documentation=https://github.com/Alexays/Waybar/wiki +PartOf=graphical-session.target +Requisite=dbus.service