diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 01c8b4be..abddaa5a 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -357,6 +357,9 @@
/modules/services/window-managers/bspwm @ncfavier
/tests/modules/services/window-managers/bspwm @ncfavier
+/modules/services/window-managers/herbstluftwm @olmokramer
+/tests/modules/services/window-managers/herbstluftwm @olmokramer
+
/modules/services/window-managers/i3-sway/i3.nix @sumnerevans
/tests/modules/services/window-managers/i3 @sumnerevans
diff --git a/modules/misc/news.nix b/modules/misc/news.nix
index d910f954..9c194d77 100644
--- a/modules/misc/news.nix
+++ b/modules/misc/news.nix
@@ -2328,6 +2328,14 @@ in
A new module is available: 'services.systembus-notify'.
'';
}
+
+ {
+ time = "2021-12-31T09:39:20+00:00";
+ condition = hostPlatform.isLinux;
+ message = ''
+ A new module is available: 'xsession.windowManager.herbstluftwm'.
+ '';
+ }
];
};
}
diff --git a/modules/modules.nix b/modules/modules.nix
index 258c329c..534d9c55 100644
--- a/modules/modules.nix
+++ b/modules/modules.nix
@@ -231,6 +231,7 @@ let
./services/volnoti.nix
./services/window-managers/awesome.nix
./services/window-managers/bspwm/default.nix
+ ./services/window-managers/herbstluftwm.nix
./services/window-managers/i3-sway/i3.nix
./services/window-managers/i3-sway/sway.nix
./services/window-managers/i3-sway/swaynag.nix
diff --git a/modules/services/window-managers/herbstluftwm.nix b/modules/services/window-managers/herbstluftwm.nix
new file mode 100644
index 00000000..efa38bce
--- /dev/null
+++ b/modules/services/window-managers/herbstluftwm.nix
@@ -0,0 +1,171 @@
+{ config, lib, pkgs, ... }:
+
+let
+
+ cfg = config.xsession.windowManager.herbstluftwm;
+
+ renderValue = val:
+ if lib.isBool val then
+ if val then "true" else "false"
+ else
+ lib.escapeShellArg val;
+
+ renderSettings = settings:
+ lib.concatStringsSep "\n" (lib.mapAttrsToList
+ (name: value: "herbstclient set ${name} ${renderValue value}") settings);
+
+ renderKeybinds = keybinds:
+ lib.concatStringsSep "\n"
+ (lib.mapAttrsToList (key: cmd: "herbstclient keybind ${key} ${cmd}")
+ keybinds);
+
+ renderMousebinds = mousebinds:
+ lib.concatStringsSep "\n"
+ (lib.mapAttrsToList (btn: cmd: "herbstclient mousebind ${btn} ${cmd}")
+ mousebinds);
+
+ renderRules = rules:
+ lib.concatStringsSep "\n" (map (rule: "herbstclient rule ${rule}") rules);
+
+ settingType = lib.types.oneOf [ lib.types.bool lib.types.int lib.types.str ];
+
+in {
+ meta.maintainers = [ lib.hm.maintainers.olmokramer ];
+
+ options.xsession.windowManager.herbstluftwm = {
+ enable = lib.mkEnableOption "herbstluftwm window manager.";
+
+ package = lib.mkOption {
+ type = lib.types.package;
+ default = pkgs.herbstluftwm;
+ defaultText = lib.literalExpression "pkgs.herbstluftwm";
+ description = ''
+ Package providing the herbstluftwm and
+ herbstclient commands.
+ '';
+ };
+
+ settings = lib.mkOption {
+ type = lib.types.attrsOf settingType;
+ default = { };
+ example = lib.literalExpression ''
+ {
+ gapless_grid = false;
+ window_border_width = 1;
+ window_border_active_color = "#FF0000";
+ }
+ '';
+ description = "Herbstluftwm settings.";
+ };
+
+ keybinds = lib.mkOption {
+ type = lib.types.attrsOf lib.types.str;
+ default = { };
+ example = lib.literalExpression ''
+ {
+ Mod4-o = "split right";
+ Mod4-u = "split bottom";
+ }
+ '';
+ description = "Herbstluftwm keybinds.";
+ };
+
+ mousebinds = lib.mkOption {
+ type = lib.types.attrsOf lib.types.str;
+ default = { };
+ example = lib.literalExpression ''
+ {
+ Mod4-B1 = "move";
+ Mod4-B3 = "resize";
+ }
+ '';
+ description = "Herbstluftwm mousebinds.";
+ };
+
+ rules = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ ];
+ example = lib.literalExpression ''
+ [
+ "windowtype~'_NET_WM_WINDOW_TYPE_(DIALOG|UTILITY|SPLASH)' focus=on pseudotile=on"
+ "windowtype~'_NET_WM_WINDOW_TYPE_(NOTIFICATION|DOCK|DESKTOP)' manage=off"
+ ]
+ '';
+ description = "Herbstluftwm rules.";
+ };
+
+ tags = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ ];
+ example = lib.literalExpression ''
+ [ "work" "browser" "music" "gaming" ]
+ '';
+ description = "Tags to create on startup.";
+ };
+
+ extraConfig = lib.mkOption {
+ type = lib.types.lines;
+ default = "";
+ example = ''
+ herbstclient set_layout max
+ herbstclient detect_monitors
+ '';
+ description = ''
+ Extra configuration lines to add verbatim to
+ $XDG_CONFIG_HOME/herbstluftwm/autostart.
+ '';
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ assertions = [
+ (lib.hm.assertions.assertPlatform "xsession.windowManager.herbstluftwm"
+ pkgs lib.platforms.linux)
+ ];
+
+ home.packages = [ cfg.package ];
+
+ xsession.windowManager.command = "${cfg.package}/bin/herbstluftwm --locked";
+
+ xdg.configFile."herbstluftwm/autostart".source =
+ pkgs.writeShellScript "herbstluftwm-autostart" ''
+ shopt -s expand_aliases
+
+ # shellcheck disable=SC2142
+ alias herbstclient='set -- "$@" ";"'
+ set --
+
+ herbstclient emit_hook reload
+
+ # Reset everything.
+ herbstclient attr theme.tiling.reset 1
+ herbstclient attr theme.floating.reset 1
+ herbstclient keyunbind --all
+ herbstclient unrule --all
+
+ ${renderSettings cfg.settings}
+
+ if ${cfg.package}/bin/herbstclient object_tree tags.by-name | ${pkgs.gnugrep}/bin/grep default; then
+ herbstclient rename default ${
+ lib.escapeShellArg (builtins.head cfg.tags)
+ }
+ fi
+
+ for tag in ${lib.escapeShellArgs cfg.tags}; do
+ herbstclient add "$tag"
+ done
+
+ ${renderKeybinds cfg.keybinds}
+
+ ${renderMousebinds cfg.mousebinds}
+
+ ${renderRules cfg.rules}
+
+ ${cfg.extraConfig}
+
+ herbstclient unlock
+
+ ${cfg.package}/bin/herbstclient chain ";" "$@"
+ '';
+ };
+}
diff --git a/tests/default.nix b/tests/default.nix
index f1445ba6..e2964179 100644
--- a/tests/default.nix
+++ b/tests/default.nix
@@ -145,6 +145,7 @@ import nmt {
./modules/services/syncthing
./modules/services/trayer
./modules/services/window-managers/bspwm
+ ./modules/services/window-managers/herbstluftwm
./modules/services/window-managers/i3
./modules/services/window-managers/sway
./modules/services/wlsunset
diff --git a/tests/modules/services/window-managers/herbstluftwm/autostart b/tests/modules/services/window-managers/herbstluftwm/autostart
new file mode 100755
index 00000000..bff4f16f
--- /dev/null
+++ b/tests/modules/services/window-managers/herbstluftwm/autostart
@@ -0,0 +1,48 @@
+#!/nix/store/00000000000000000000000000000000-bash-5.1-p12/bin/bash
+shopt -s expand_aliases
+
+# shellcheck disable=SC2142
+alias herbstclient='set -- "$@" ";"'
+set --
+
+herbstclient emit_hook reload
+
+# Reset everything.
+herbstclient attr theme.tiling.reset 1
+herbstclient attr theme.floating.reset 1
+herbstclient keyunbind --all
+herbstclient unrule --all
+
+herbstclient set always_show_frame true
+herbstclient set default_frame_layout 'max'
+herbstclient set frame_bg_active_color '#000000'
+herbstclient set frame_gap '12'
+herbstclient set frame_padding '-12'
+
+if @herbstluftwm@/bin/herbstclient object_tree tags.by-name | /nix/store/00000000000000000000000000000000-gnugrep-3.7/bin/grep default; then
+ herbstclient rename default '1'
+fi
+
+for tag in '1' 'with space' 'wə1rd#ch@rs'\'''; do
+ herbstclient add "$tag"
+done
+
+herbstclient keybind Mod4-1 use 1
+herbstclient keybind Mod4-2 use 2
+herbstclient keybind Mod4-Alt-Tab cycle -1
+herbstclient keybind Mod4-Tab cycle 1
+
+herbstclient mousebind Mod4-B1 move
+herbstclient mousebind Mod4-B3 resize
+
+herbstclient rule focus=on
+herbstclient rule windowtype~'_NET_WM_WINDOW_TYPE_(DIALOG|UTILITY|SPLASH)' focus=on pseudotile=on
+herbstclient rule class~'[Pp]inentry' instance=pinentry focus=on floating=on floatplacement=center keys_inactive='.*'
+
+herbstclient use 1
+
+
+herbstclient unlock
+
+@herbstluftwm@/bin/herbstclient chain ";" "$@"
+
diff --git a/tests/modules/services/window-managers/herbstluftwm/default.nix b/tests/modules/services/window-managers/herbstluftwm/default.nix
new file mode 100644
index 00000000..04b9f89c
--- /dev/null
+++ b/tests/modules/services/window-managers/herbstluftwm/default.nix
@@ -0,0 +1 @@
+{ herbstluftwm-simple-config = ./herbstluftwm-simple-config.nix; }
diff --git a/tests/modules/services/window-managers/herbstluftwm/herbstluftwm-simple-config.nix b/tests/modules/services/window-managers/herbstluftwm/herbstluftwm-simple-config.nix
new file mode 100644
index 00000000..27e87150
--- /dev/null
+++ b/tests/modules/services/window-managers/herbstluftwm/herbstluftwm-simple-config.nix
@@ -0,0 +1,44 @@
+{ lib, pkgs, ... }:
+
+{
+ xsession.windowManager.herbstluftwm = {
+ enable = true;
+ settings = {
+ always_show_frame = true;
+ default_frame_layout = "max";
+ frame_bg_active_color = "#000000";
+ frame_gap = 12;
+ frame_padding = -12;
+ };
+ keybinds = {
+ "Mod4-1" = "use 1";
+ "Mod4-2" = "use 2";
+ "Mod4-Tab" = "cycle 1";
+ "Mod4-Alt-Tab" = "cycle -1";
+ };
+ mousebinds = {
+ "Mod4-B1" = "move";
+ "Mod4-B3" = "resize";
+ };
+ rules = [
+ "focus=on"
+ "windowtype~'_NET_WM_WINDOW_TYPE_(DIALOG|UTILITY|SPLASH)' focus=on pseudotile=on"
+ "class~'[Pp]inentry' instance=pinentry focus=on floating=on floatplacement=center keys_inactive='.*'"
+ ];
+ tags = [ "1" "with space" "wə1rd#ch@rs'" ];
+ extraConfig = ''
+ herbstclient use 1
+ '';
+ };
+
+ test.stubs.herbstluftwm = { };
+
+ nmt.script = ''
+ autostart=home-files/.config/herbstluftwm/autostart
+ assertFileExists "$autostart"
+ assertFileIsExecutable "$autostart"
+
+ normalizedAutostart=$(normalizeStorePaths "$autostart")
+ assertFileContent "$normalizedAutostart" ${./autostart}
+ '';
+}