diff --git a/modules/misc/news.nix b/modules/misc/news.nix index 98e9c2beb..117ff6cb7 100644 --- a/modules/misc/news.nix +++ b/modules/misc/news.nix @@ -1371,6 +1371,14 @@ in A new module is available: 'programs.bemenu'. ''; } + + { + time = "2024-01-01T09:09:42+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'programs.i3blocks'. + ''; + } ]; }; } diff --git a/modules/modules.nix b/modules/modules.nix index 1dbb04601..ece9c5cec 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -110,6 +110,7 @@ let ./programs/hstr.nix ./programs/htop.nix ./programs/hyfetch.nix + ./programs/i3blocks.nix ./programs/i3status-rust.nix ./programs/i3status.nix ./programs/imv.nix diff --git a/modules/programs/i3blocks.nix b/modules/programs/i3blocks.nix new file mode 100644 index 000000000..e3d8d6812 --- /dev/null +++ b/modules/programs/i3blocks.nix @@ -0,0 +1,114 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.i3blocks; + + # Re-make the atom type for the INI files. + # For some reason, the normal INI type seems to be incompatible with + # DAG + configAtomType = let + # Keep the INI atom type here + optType = with types; (nullOr (oneOf [ int bool str float ])); + in types.mkOptionType { + name = "INI config atom"; + description = "INI atom (null, int, bool, string, or float)"; + check = x: optType.check x; + merge = (loc: defs: (optType.merge loc defs)); + }; + + # Create the type of the actual config type + configType = types.attrsOf configAtomType; + + # The INI generator + mkIni = generators.toINI { }; + +in { + meta.maintainers = [ maintainers.noodlez1232 ]; + + options.programs.i3blocks = { + enable = mkEnableOption "i3blocks i3 status command scheduler"; + + package = mkOption { + type = types.package; + default = pkgs.i3blocks; + defaultText = literalExpression "pkgs.i3blocks"; + description = "Package providing {command}`i3blocks`."; + }; + + bars = mkOption { + type = with types; attrsOf (hm.types.dagOf configType); + description = "Configuration written to i3blocks config"; + example = literalExpression '' + { + top = { + # The title block + title = { + interval = "persist"; + command = "xtitle -s"; + }; + }; + bottom = { + time = { + command = "date +%r"; + interval = 1; + }; + # Make sure this block comes after the time block + date = lib.hm.dag.entryAfter [ "time" ] { + command = "date +%d"; + interval = 5; + }; + # And this block after the example block + example = lib.hm.dag.entryAfter [ "date" ] { + command = "echo hi $(date +%s)"; + interval = 3; + }; + }; + }''; + }; + }; + + config = let + # A function to create the file that will be put into the XDG config home. + makeFile = config: + let + # Takes a singular name value pair and turns it into an attrset + nameValuePairToAttr = value: (builtins.listToAttrs [ value ]); + # Converts a dag entry to a name-value pair + dagEntryToNameValue = entry: (nameValuePair entry.name entry.data); + + # Try to sort the blocks + trySortedBlocks = hm.dag.topoSort config; + + # Get the blocks if successful, abort if not + blocks = if trySortedBlocks ? result then + trySortedBlocks.result + else + abort + "Dependency cycle in i3blocks: ${builtins.toJSON trySortedBlocks}"; + + # Turn the blocks back into their name value pairs + orderedBlocks = + (map (value: (nameValuePairToAttr (dagEntryToNameValue value))) + blocks); + in { + # We create an "INI" file for each bar, then append them all in order + text = concatStringsSep "\n" (map (value: (mkIni value)) orderedBlocks); + }; + + # Make our config (if enabled + in mkIf cfg.enable { + assertions = [ + (lib.hm.assertions.assertPlatform "programs.i3blocks" pkgs + lib.platforms.linux) + ]; + + home.packages = [ cfg.package ]; + + xdg.configFile = (mapAttrs' + (name: value: nameValuePair "i3blocks/${name}" (makeFile value)) + cfg.bars); + }; +} diff --git a/tests/default.nix b/tests/default.nix index 77dcbe0f2..d5a5be706 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -190,6 +190,7 @@ import nmt { ./modules/programs/getmail ./modules/programs/gnome-terminal ./modules/programs/hexchat + ./modules/programs/i3blocks ./modules/programs/i3status-rust ./modules/programs/imv ./modules/programs/kodi diff --git a/tests/modules/programs/i3blocks/default.nix b/tests/modules/programs/i3blocks/default.nix new file mode 100644 index 000000000..f03eb3909 --- /dev/null +++ b/tests/modules/programs/i3blocks/default.nix @@ -0,0 +1 @@ +{ i3blocks-with-ordered-blocks = ./with-ordered-blocks.nix; } diff --git a/tests/modules/programs/i3blocks/with-ordered-blocks.nix b/tests/modules/programs/i3blocks/with-ordered-blocks.nix new file mode 100644 index 000000000..c2c9e6477 --- /dev/null +++ b/tests/modules/programs/i3blocks/with-ordered-blocks.nix @@ -0,0 +1,65 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + expectedConfig = pkgs.writeText "i3blocks-expected-config" '' + [block1first] + command=echo first + interval=1 + + [block3second] + command=echo second + interval=2 + + [block2third] + command=echo third + interval=3 + ''; +in { + config = { + programs.i3blocks = { + enable = true; + package = config.lib.test.mkStubPackage { }; + bars = with lib; { + bar1 = { + block1first = { + command = "echo first"; + interval = 1; + }; + block2third = hm.dag.entryAfter [ "block3second" ] { + command = "echo third"; + interval = 3; + }; + block3second = hm.dag.entryAfter [ "block1first" ] { + command = "echo second"; + interval = 2; + }; + }; + bar2 = { + block1first = { + command = "echo first"; + interval = 1; + }; + block2third = hm.dag.entryAfter [ "block3second" ] { + command = "echo third"; + interval = 3; + }; + block3second = hm.dag.entryAfter [ "block1first" ] { + command = "echo second"; + interval = 2; + }; + }; + }; + }; + + test.stubs.i3blocks = { }; + + nmt.script = '' + assertFileExists home-files/.config/i3blocks/bar1 + assertFileExists home-files/.config/i3blocks/bar2 + assertFileContent home-files/.config/i3blocks/bar1 ${expectedConfig} + assertFileContent home-files/.config/i3blocks/bar2 ${expectedConfig} + ''; + }; +}