diff --git a/home-manager/default.nix b/home-manager/default.nix index a58539617..bb0988691 100644 --- a/home-manager/default.nix +++ b/home-manager/default.nix @@ -23,6 +23,7 @@ pkgs.stdenv.mkDerivation { substituteInPlace $out/bin/home-manager \ --subst-var-by bash "${pkgs.bash}" \ --subst-var-by coreutils "${pkgs.coreutils}" \ + --subst-var-by less "${pkgs.less}" \ --subst-var-by MODULES_PATH '${modulesPathStr}' \ --subst-var-by HOME_MANAGER_EXPR_PATH "${./home-manager.nix}" ''; diff --git a/home-manager/home-manager b/home-manager/home-manager index 8e0bd40c2..4722d41a2 100644 --- a/home-manager/home-manager +++ b/home-manager/home-manager @@ -3,12 +3,12 @@ # This code explicitly requires GNU Core Utilities and we therefore # need to ensure they are prioritized over any other similarly named # tools on the system. -PATH=@coreutils@/bin:$PATH +PATH=@coreutils@/bin:@less@/bin${PATH:+:}$PATH set -euo pipefail function errorEcho() { - >&2 echo "$*" + echo $* >&2 } # Attempts to set the HOME_MANAGER_CONFIG global variable. @@ -52,12 +52,11 @@ function setHomeManagerModulesPath() { done } -function doBuild() { +function doBuildAttr() { setConfigFile setHomeManagerModulesPath - local extraArgs - extraArgs="$1" + local extraArgs="$*" for p in "${EXTRA_NIX_PATH[@]}"; do extraArgs="$extraArgs -I $p" @@ -67,11 +66,53 @@ function doBuild() { extraArgs="$extraArgs --show-trace" fi - nix-build $extraArgs \ - "@HOME_MANAGER_EXPR_PATH@" \ - --argstr confPath "$HOME_MANAGER_CONFIG" \ - --argstr confAttr "$HOME_MANAGER_CONFIG_ATTRIBUTE" \ - -A activationPackage + nix-build \ + "@HOME_MANAGER_EXPR_PATH@" \ + $extraArgs \ + --argstr confPath "$HOME_MANAGER_CONFIG" \ + --argstr confAttr "$HOME_MANAGER_CONFIG_ATTRIBUTE" +} + +function presentNews() { + local infoFile + infoFile=$(doBuildNews -A newsInfo) || return 1 + + # shellcheck source=/dev/null + . "$infoFile" + + if [[ $newsNumUnread -eq 0 ]]; then + return + elif [[ "$newsDisplay" == "silent" ]]; then + return + elif [[ "$newsDisplay" == "notify" ]]; then + local msg + if [[ $newsNumUnread -eq 1 ]]; then + msg="There is an unread and relevant news item.\n" + msg+="Read it by running the command '$(basename "$0") news'." + else + msg="There are $newsNumUnread unread and relevant news items.\n" + msg+="Read them by running the command '$(basename "$0") news'." + fi + + # Not actually an error but here stdout is reserved for + # nix-build output. + errorEcho + errorEcho -e "$msg" + errorEcho + + if [[ -v DISPLAY ]] && type -P notify-send > /dev/null; then + notify-send "Home Manager" "$msg" + fi + elif [[ "$newsDisplay" == "show" ]]; then + doShowNews --unread + else + errorEcho "Unknown 'news.display' setting '$newsDisplay'." + fi +} + +function doBuild() { + doBuildAttr -A activationPackage + presentNews } function doSwitch() { @@ -84,12 +125,17 @@ function doSwitch() { # prevents an unfortunately timed GC from removing the generation # before activation completes. wrkdir="$(mktemp -d)" - generation=$(doBuild "-o $wrkdir/result") && $generation/activate || exitCode=1 + generation=$(doBuildAttr -o "$wrkdir/result" -A activationPackage) \ + && $generation/activate || exitCode=1 # Because the previous command never fails, the script keeps # running and $wrkdir is always removed. rm -r "$wrkdir" + if [[ $exitCode -eq 0 ]]; then + presentNews + fi + return $exitCode } @@ -110,6 +156,53 @@ function doListPackages() { fi } +function newsReadIdsFile() { + local dataDir="${XDG_DATA_HOME:-$HOME/.local/share}/home-manager" + local path="$dataDir/news-read-ids" + + # If the path doesn't exist then we should create it, otherwise + # Nix will error out when we attempt to use builtins.readFile. + if [[ ! -f "$path" ]]; then + mkdir -p "$dataDir" + touch "$path" + fi + + echo "$path" +} + +function doBuildNews() { + doBuildAttr "$*" \ + --no-out-link \ + --arg check false \ + --argstr newsReadIdsFile "$(newsReadIdsFile)" +} + +function doShowNews() { + local infoFile + infoFile=$(doBuildNews -A newsInfo) || return 1 + + # shellcheck source=/dev/null + . "$infoFile" + + case $1 in + --all) + ${PAGER:-less} "$newsFileAll" + ;; + --unread) + ${PAGER:-less} "$newsFileUnread" + ;; + *) + errorEcho "Unknown argument $1" + return 1 + esac + + if [[ -s "$newsUnreadIdsFile" ]]; then + local newsReadIdsFile + newsReadIdsFile="$(newsReadIdsFile)" + cat "$newsUnreadIdsFile" >> "$newsReadIdsFile" + fi +} + function doHelp() { echo "Usage: $0 [OPTION] COMMAND" echo @@ -130,6 +223,7 @@ function doHelp() { echo " switch Build and activate configuration" echo " generations List all home environment generations" echo " packages List all packages installed in home-manager-path" + echo " news Show news entries in a pager" } EXTRA_NIX_PATH=() @@ -171,7 +265,7 @@ cmd="$*" case "$cmd" in build) - doBuild "" + doBuild ;; switch) doSwitch @@ -182,6 +276,9 @@ case "$cmd" in packages) doListPackages ;; + news) + doShowNews --all + ;; help|--help) doHelp ;; diff --git a/home-manager/home-manager.nix b/home-manager/home-manager.nix index f356b5a0c..a6c9259f8 100644 --- a/home-manager/home-manager.nix +++ b/home-manager/home-manager.nix @@ -1,6 +1,14 @@ -{ pkgs ? import {}, confPath, confAttr }: +{ pkgs ? import {} +, confPath +, confAttr +, check ? true +, newsReadIdsFile ? null +}: + +with pkgs.lib; let + env = import { configuration = let @@ -8,8 +16,74 @@ let in if confAttr == "" then conf else conf.${confAttr}; pkgs = pkgs; + check = check; }; + + newsReadIds = + if newsReadIdsFile == null + then {} + else + let + ids = splitString "\n" (fileContents newsReadIdsFile); + in + builtins.listToAttrs (map (id: { name = id; value = null; }) ids); + + newsIsRead = entry: builtins.hasAttr entry.id newsReadIds; + + newsFiltered = + let + pred = entry: entry.condition && ! newsIsRead entry; + in + filter pred env.newsEntries; + + newsNumUnread = length newsFiltered; + + newsFileUnread = pkgs.writeText "news-unread.txt" ( + concatMapStringsSep "\n\n" (entry: + let + time = replaceStrings ["T"] [" "] (removeSuffix "+00:00" entry.time); + in + '' + * ${time} + + ${replaceStrings ["\n"] ["\n "] entry.message} + '' + ) newsFiltered + ); + + newsFileAll = pkgs.writeText "news-all.txt" ( + concatMapStringsSep "\n\n" (entry: + let + flag = if newsIsRead entry then "read" else "unread"; + time = replaceStrings ["T"] [" "] (removeSuffix "+00:00" entry.time); + in + '' + * ${time} [${flag}] + + ${replaceStrings ["\n"] ["\n "] entry.message} + '' + ) env.newsEntries + ); + + # File where each line corresponds to an unread news entry + # identifier. If non-empty then the file ends in "\n". + newsUnreadIdsFile = pkgs.writeText "news-unread-ids" ( + let + text = concatMapStringsSep "\n" (entry: entry.id) newsFiltered; + in + text + optionalString (text != "") "\n" + ); + + newsInfo = pkgs.writeText "news-info.sh" '' + local newsNumUnread=${toString newsNumUnread} + local newsDisplay="${env.newsDisplay}" + local newsFileAll="${newsFileAll}" + local newsFileUnread="${newsFileUnread}" + local newsUnreadIdsFile="${newsUnreadIdsFile}" + ''; + in { inherit (env) activationPackage; + inherit newsInfo; } diff --git a/modules/default.nix b/modules/default.nix index 7add9dc3b..9137fa384 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -1,6 +1,9 @@ { configuration , pkgs , lib ? pkgs.stdenv.lib + + # Whether to check that each option has a matching declaration. +, check ? true }: with lib; @@ -64,6 +67,7 @@ let pkgsModule = { config._module.args.pkgs = lib.mkForce pkgs; config._module.args.baseModules = modules; + config._module.check = check; }; module = showWarnings ( @@ -90,4 +94,10 @@ in # For backwards compatibility. Please use activationPackage instead. activation-script = module.config.home.activationPackage; + + newsDisplay = module.config.news.display; + newsEntries = + sort (a: b: a.time > b.time) ( + filter (a: a.condition) module.config.news.entries + ); }