aboutsummaryrefslogtreecommitdiff
path: root/home-manager/modules/programs/waybar.nix
{ config, lib, pkgs, ... }:

with lib;
let
  cfg = config.programs.waybar;

  # Used when generating warnings
  modulesPath = "programs.waybar.settings.[].modules";

  # Taken from <https://github.com/Alexays/Waybar/blob/adaf84304865e143e4e83984aaea6f6a7c9d4d96/src/factory.cpp>
  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 (<code>"top"</code>)
            of the windows or behind (<code>"bottom"</code>).
          '';
          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 <code>null</code> to use the default module.
      '';
    };

    settings = mkOption {
      type = listOf waybarBarConfig;
      default = [ ];
      description = ''
        Configuration for Waybar, see <link
          xlink:href="https://github.com/Alexays/Waybar/wiki/Configuration"/>
        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 <link xlink:href="https://github.com/Alexays/Waybar/wiki/Configuration"/>
        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" ]; };
      };
    })
  ]);
}