nix/modules/login-display.nix
2025-10-11 19:16:15 +02:00

461 lines
22 KiB
Nix
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.login-display;
in
{
options.services.login-display = {
enable = mkEnableOption "login information display on SSH sessions";
showSmartStatus = mkOption {
type = types.bool;
default = false;
description = "Show SMART disk health status";
};
smartDrives = mkOption {
type = types.attrsOf types.str;
default = {};
description = "Drives to monitor for SMART status (device path -> name mapping)";
example = {
"/dev/disk/by-id/ata-Samsung_SSD" = "System_SSD";
};
};
showSystemInfo = mkOption {
type = types.bool;
default = true;
description = "Show basic system information (hostname, uptime, load)";
};
showDiskUsage = mkOption {
type = types.bool;
default = false;
description = "Show disk usage information";
};
diskUsagePaths = mkOption {
type = types.listOf types.str;
default = [ "/" ];
description = "Paths to check for disk usage";
};
showBorgStatus = mkOption {
type = types.bool;
default = false;
description = "Show last borg backup status";
};
showContainerUpdater = mkOption {
type = types.bool;
default = false;
description = "Show last container updater status";
};
};
config = mkIf cfg.enable {
# Add smartmontools if SMART status is enabled
environment.systemPackages = mkIf cfg.showSmartStatus [ pkgs.smartmontools ];
# Configure shell login initialization
programs.zsh.loginShellInit = mkIf config.programs.zsh.enable (
let
# ANSI color codes for truecolor (using \033 for better compatibility)
colors = {
reset = "\\033[0m";
dim = "\\033[2m";
cyan = "\\033[38;2;0;200;255m";
blue = "\\033[38;2;100;150;255m";
green = "\\033[38;2;80;250;123m";
yellow = "\\033[38;2;241;250;140m";
orange = "\\033[38;2;255;184;108m";
red = "\\033[38;2;255;85;85m";
gray = "\\033[38;2;100;100;120m";
};
# Build SMART status display
smartStatusCode = optionalString cfg.showSmartStatus ''
${concatStringsSep "\n" (mapAttrsToList (device: name: ''
if [[ -e "${device}" ]]; then
# Get health status
if [[ "${device}" == *"nvme"* ]]; then
HEALTH_OUTPUT=$(sudo ${pkgs.smartmontools}/bin/smartctl -d nvme -H "${device}" 2>/dev/null)
else
HEALTH_OUTPUT=$(sudo ${pkgs.smartmontools}/bin/smartctl -H "${device}" 2>/dev/null)
fi
if HEALTH=$(echo "$HEALTH_OUTPUT" | ${pkgs.gnugrep}/bin/grep -o "PASSED\|FAILED" | head -1); then
: # HEALTH is set
else
HEALTH="UNKNOWN"
fi
# Get temperature
TEMP="N/A"
if [[ "$HEALTH" == "PASSED" ]]; then
if [[ "${device}" == *"nvme"* ]]; then
SMART_DATA=$(sudo ${pkgs.smartmontools}/bin/smartctl -d nvme -A "${device}" 2>/dev/null)
TEMP=$(echo "$SMART_DATA" | ${pkgs.gawk}/bin/awk '/^Temperature:/ {print $2}' | head -1)
[[ -n "$TEMP" && "$TEMP" =~ ^[0-9]+$ ]] && TEMP="''${TEMP}" || TEMP="N/A"
else
SMART_DATA=$(sudo ${pkgs.smartmontools}/bin/smartctl -A "${device}" 2>/dev/null)
TEMP=$(echo "$SMART_DATA" | ${pkgs.gawk}/bin/awk '/Temperature_Celsius/ {print $10}' | head -1)
[[ -n "$TEMP" && "$TEMP" =~ ^[0-9]+$ ]] && TEMP="''${TEMP}" || TEMP="N/A"
fi
fi
# Color-code status and temperature
if [[ "$HEALTH" == "PASSED" ]]; then
STATUS="\\033[38;2;80;250;123m\\033[0m"
HEALTH_COLOR="\\033[38;2;80;250;123m"
# Color temp based on value
if [[ "$TEMP" =~ ^[0-9]+$ ]]; then
if [[ $TEMP -ge 70 ]]; then
TEMP_COLOR="\\033[38;2;255;85;85m"
elif [[ $TEMP -ge 50 ]]; then
TEMP_COLOR="\\033[38;2;255;184;108m"
else
TEMP_COLOR="\\033[38;2;241;250;140m"
fi
TEMP_STR="$(printf "%b" "''${TEMP_COLOR}''${TEMP}°C\\033[0m")"
else
TEMP_STR="$(printf "%b" "\\033[2m$TEMP\\033[0m")"
fi
elif [[ "$HEALTH" == "FAILED" ]]; then
STATUS="\\033[38;2;255;85;85m\\033[0m"
HEALTH_COLOR="\\033[38;2;255;85;85m"
TEMP_STR="$(printf "%b" "\\033[2m$TEMP\\033[0m")"
else
STATUS="\\033[38;2;241;250;140m\\033[0m"
HEALTH_COLOR="\\033[38;2;241;250;140m"
TEMP_STR="$(printf "%b" "\\033[2m$TEMP\\033[0m")"
fi
printf " %b \\033[2m%-15s\\033[0m %b%-7s\\033[0m %s\n" "$STATUS" "${name}" "$HEALTH_COLOR" "$HEALTH" "$TEMP_STR"
else
printf " \\033[38;2;241;250;140m\\033[0m \\033[2m%-15s\\033[0m \\033[38;2;255;85;85m%-20s\\033[0m\n" "${name}" "Not found"
fi
'') cfg.smartDrives)}
'';
# Build system info display
systemInfoCode = optionalString cfg.showSystemInfo ''
# Parse uptime
UPTIME_STR=$(uptime | ${pkgs.gawk}/bin/awk '{
match($0, /up\s+(.+?),\s+[0-9]+\s+user/, arr)
if (arr[1] != "") {
gsub(/^ +| +$/, "", arr[1])
# Shorten format: "5 days, 3:42" -> "5d 3h"
gsub(/ days?,/, "d", arr[1])
gsub(/ hours?,/, "h", arr[1])
gsub(/ mins?,/, "m", arr[1])
gsub(/:[0-9]+$/, "", arr[1])
print arr[1]
}
}')
LOAD=$(uptime | ${pkgs.gawk}/bin/awk -F'load average:' '{gsub(/^ +| +$/, "", $2); print $2}')
printf " \\033[38;2;0;200;255m%s\\033[0m \\033[2m·\\033[0m \\033[2m\\033[0m %s \\033[2m· load\\033[0m %s\n" "$(hostname)" "$UPTIME_STR" "$LOAD"
'';
# Build disk usage display with bar
diskUsageCode = optionalString cfg.showDiskUsage ''
${concatMapStringsSep "\n" (path: ''
DF_OUTPUT=$(df -h "${path}" | ${pkgs.gawk}/bin/awk 'NR==2 {print $3, $2, $5}')
read -r USED TOTAL PCT <<< "$DF_OUTPUT"
PCT_NUM=''${PCT%\%}
# Create progress bar (10 chars)
FILLED=$((PCT_NUM / 10))
EMPTY=$((10 - FILLED))
BAR=""
for ((i=0; i<FILLED; i++)); do BAR="$BAR"; done
for ((i=0; i<EMPTY; i++)); do BAR="$BAR"; done
# Color bar based on usage
if [[ $PCT_NUM -ge 90 ]]; then
BAR_COLOR="\\033[38;2;255;85;85m"
elif [[ $PCT_NUM -ge 70 ]]; then
BAR_COLOR="\\033[38;2;255;184;108m"
elif [[ $PCT_NUM -ge 50 ]]; then
BAR_COLOR="\\033[38;2;241;250;140m"
else
BAR_COLOR="\\033[38;2;80;250;123m"
fi
printf " \\033[2m%-12s\\033[0m %6s/%-6s %b%s\\033[0m %5s\n" "${path}" "$USED" "$TOTAL" "$BAR_COLOR" "$BAR" "$PCT"
'') cfg.diskUsagePaths}
'';
# Build container updater status display
containerUpdaterStatusCode = optionalString cfg.showContainerUpdater ''
# Query journalctl for container-updater.service
CONTAINER_LOG=$(journalctl -u container-updater.service -n 150 --no-pager --output=cat 2>/dev/null || echo "")
if [[ -z "$CONTAINER_LOG" ]]; then
# Service never ran
printf " \\033[38;2;241;250;140m\\033[0m \\033[2mNever run\\033[0m\n"
else
# Check if last update completed
if echo "$CONTAINER_LOG" | ${pkgs.gnugrep}/bin/grep -q "Container update completed successfully"; then
STATUS_SYMBOL="\\033[38;2;80;250;123m\\033[0m"
STATUS_COLOR="\\033[38;2;80;250;123m"
STATUS_TEXT="SUCCESS"
# Get timestamp of last successful update
LAST_TIMESTAMP=$(journalctl -u container-updater.service --output=short-iso -n 150 --no-pager 2>/dev/null | ${pkgs.gnugrep}/bin/grep "Container update completed successfully" | tail -1 | ${pkgs.gawk}/bin/awk '{print $1}')
if [[ -n "$LAST_TIMESTAMP" ]]; then
# Calculate time ago
LAST_EPOCH=$(date -d "$LAST_TIMESTAMP" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S%z" "$LAST_TIMESTAMP" +%s 2>/dev/null || echo "0")
NOW_EPOCH=$(date +%s)
DIFF_SECONDS=$((NOW_EPOCH - LAST_EPOCH))
if [[ $DIFF_SECONDS -lt 3600 ]]; then
TIME_AGO="$((DIFF_SECONDS / 60))m ago"
elif [[ $DIFF_SECONDS -lt 86400 ]]; then
TIME_AGO="$((DIFF_SECONDS / 3600))h ago"
else
TIME_AGO="$((DIFF_SECONDS / 86400))d ago"
fi
# Adjust color based on age
if [[ $DIFF_SECONDS -gt 172800 ]]; then
# > 48h - red
STATUS_SYMBOL="\\033[38;2;255;85;85m\\033[0m"
STATUS_COLOR="\\033[38;2;255;85;85m"
elif [[ $DIFF_SECONDS -gt 86400 ]]; then
# 24-48h - yellow
STATUS_SYMBOL="\\033[38;2;241;250;140m\\033[0m"
STATUS_COLOR="\\033[38;2;241;250;140m"
fi
else
TIME_AGO="Unknown"
fi
# Extract container counts from summary
UPDATED_COUNT=$(echo "$CONTAINER_LOG" | ${pkgs.gnugrep}/bin/grep -oP " Updated \(\K[0-9]+" | tail -1)
FAILED_COUNT=$(echo "$CONTAINER_LOG" | ${pkgs.gnugrep}/bin/grep -oP " Failed \(\K[0-9]+" | tail -1)
SKIPPED_COUNT=$(echo "$CONTAINER_LOG" | ${pkgs.gnugrep}/bin/grep -oP " No updates \(\K[0-9]+" | tail -1)
# Build summary text
SUMMARY_PARTS=()
[[ -n "$UPDATED_COUNT" && "$UPDATED_COUNT" != "0" ]] && SUMMARY_PARTS+=("$UPDATED_COUNT updated")
[[ -n "$FAILED_COUNT" && "$FAILED_COUNT" != "0" ]] && SUMMARY_PARTS+=("$FAILED_COUNT failed")
[[ -n "$SKIPPED_COUNT" && "$SKIPPED_COUNT" != "0" ]] && SUMMARY_PARTS+=("$SKIPPED_COUNT skipped")
SUMMARY=$(IFS=", "; echo "''${SUMMARY_PARTS[*]}")
[[ -z "$SUMMARY" ]] && SUMMARY="No containers"
# Display main status line
printf " %b \\033[2mLast update\\033[0m %b%s\\033[0m \\033[2m%s\\033[0m\n" "$STATUS_SYMBOL" "$STATUS_COLOR" "$TIME_AGO" "$SUMMARY"
# Extract and display updated containers (only those actually updated, not skipped)
if [[ -n "$UPDATED_COUNT" && "$UPDATED_COUNT" != "0" ]]; then
UPDATED_LIST=$(echo "$CONTAINER_LOG" | ${pkgs.gawk}/bin/awk '/ Updated \([0-9]+\):/,/^(||=|$)/ {if ($0 ~ /^ / && $0 !~ /\(no update\)/) {gsub(/^ /, ""); print}}' | tr '\n' ', ' | sed 's/, $//')
if [[ -n "$UPDATED_LIST" ]]; then
printf " \\033[2m Updated: %s\\033[0m\n" "$UPDATED_LIST"
fi
fi
# Extract and display failed containers
if [[ -n "$FAILED_COUNT" && "$FAILED_COUNT" != "0" ]]; then
FAILED_LIST=$(echo "$CONTAINER_LOG" | ${pkgs.gawk}/bin/awk '/ Failed \([0-9]+\):/,/^(||=|$)/ {if ($0 ~ /^ /) {gsub(/^ /, ""); print}}' | tr '\n' ', ' | sed 's/, $//')
if [[ -n "$FAILED_LIST" ]]; then
printf " \\033[38;2;255;85;85m Failed: %s\\033[0m\n" "$FAILED_LIST"
fi
fi
elif echo "$CONTAINER_LOG" | ${pkgs.gnugrep}/bin/grep -q "ERROR: Some containers failed to update"; then
# Update ran but had failures
STATUS_SYMBOL="\\033[38;2;255;85;85m\\033[0m"
STATUS_COLOR="\\033[38;2;255;85;85m"
# Get timestamp
LAST_TIMESTAMP=$(journalctl -u container-updater.service --output=short-iso -n 150 --no-pager 2>/dev/null | ${pkgs.gnugrep}/bin/grep "ERROR: Some containers failed to update" | tail -1 | ${pkgs.gawk}/bin/awk '{print $1}')
if [[ -n "$LAST_TIMESTAMP" ]]; then
LAST_EPOCH=$(date -d "$LAST_TIMESTAMP" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S%z" "$LAST_TIMESTAMP" +%s 2>/dev/null || echo "0")
NOW_EPOCH=$(date +%s)
DIFF_SECONDS=$((NOW_EPOCH - LAST_EPOCH))
if [[ $DIFF_SECONDS -lt 3600 ]]; then
TIME_AGO="$((DIFF_SECONDS / 60))m ago"
elif [[ $DIFF_SECONDS -lt 86400 ]]; then
TIME_AGO="$((DIFF_SECONDS / 3600))h ago"
else
TIME_AGO="$((DIFF_SECONDS / 86400))d ago"
fi
else
TIME_AGO="Unknown"
fi
# Extract counts
UPDATED_COUNT=$(echo "$CONTAINER_LOG" | ${pkgs.gnugrep}/bin/grep -oP " Updated \(\K[0-9]+" | tail -1)
FAILED_COUNT=$(echo "$CONTAINER_LOG" | ${pkgs.gnugrep}/bin/grep -oP " Failed \(\K[0-9]+" | tail -1)
SUMMARY_PARTS=()
[[ -n "$FAILED_COUNT" && "$FAILED_COUNT" != "0" ]] && SUMMARY_PARTS+=("$FAILED_COUNT failed")
[[ -n "$UPDATED_COUNT" && "$UPDATED_COUNT" != "0" ]] && SUMMARY_PARTS+=("$UPDATED_COUNT updated")
SUMMARY=$(IFS=", "; echo "''${SUMMARY_PARTS[*]}")
printf " %b \\033[2mLast update\\033[0m %b%s\\033[0m \\033[38;2;255;85;85m%s\\033[0m\n" "$STATUS_SYMBOL" "$STATUS_COLOR" "$TIME_AGO" "$SUMMARY"
# Show updated containers
if [[ -n "$UPDATED_COUNT" && "$UPDATED_COUNT" != "0" ]]; then
UPDATED_LIST=$(echo "$CONTAINER_LOG" | ${pkgs.gawk}/bin/awk '/ Updated \([0-9]+\):/,/^(||=|$)/ {if ($0 ~ /^ / && $0 !~ /\(no update\)/) {gsub(/^ /, ""); print}}' | tr '\n' ', ' | sed 's/, $//')
if [[ -n "$UPDATED_LIST" ]]; then
printf " \\033[2m Updated: %s\\033[0m\n" "$UPDATED_LIST"
fi
fi
# Show failed containers
if [[ -n "$FAILED_COUNT" && "$FAILED_COUNT" != "0" ]]; then
FAILED_LIST=$(echo "$CONTAINER_LOG" | ${pkgs.gawk}/bin/awk '/ Failed \([0-9]+\):/,/^(||=|$)/ {if ($0 ~ /^ /) {gsub(/^ /, ""); print}}' | tr '\n' ', ' | sed 's/, $//')
if [[ -n "$FAILED_LIST" ]]; then
printf " \\033[38;2;255;85;85m Failed: %s\\033[0m\n" "$FAILED_LIST"
fi
fi
else
# Unknown or no completion message
STATUS_SYMBOL="\\033[38;2;241;250;140m\\033[0m"
STATUS_TEXT="Unknown"
printf " %b \\033[2mLast update\\033[0m \\033[38;2;241;250;140m%s\\033[0m\n" "$STATUS_SYMBOL" "$STATUS_TEXT"
fi
fi
'';
# Build borg backup status display
borgStatusCode = optionalString cfg.showBorgStatus ''
# Query journalctl for borg-backup.service
BORG_LOG=$(journalctl -u borg-backup.service -n 100 --no-pager --output=cat 2>/dev/null || echo "")
if [[ -z "$BORG_LOG" ]]; then
# Service never ran
printf " \\033[38;2;241;250;140m\\033[0m \\033[2mNever run\\033[0m\n"
else
# Check if last backup succeeded
if echo "$BORG_LOG" | ${pkgs.gnugrep}/bin/grep -q "Backup process completed successfully"; then
STATUS_SYMBOL="\\033[38;2;80;250;123m\\033[0m"
STATUS_COLOR="\\033[38;2;80;250;123m"
# Get timestamp of last successful backup
LAST_TIMESTAMP=$(journalctl -u borg-backup.service --output=short-iso -n 100 --no-pager 2>/dev/null | ${pkgs.gnugrep}/bin/grep "Backup process completed successfully" | tail -1 | ${pkgs.gawk}/bin/awk '{print $1}')
if [[ -n "$LAST_TIMESTAMP" ]]; then
# Calculate time ago
LAST_EPOCH=$(date -d "$LAST_TIMESTAMP" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S%z" "$LAST_TIMESTAMP" +%s 2>/dev/null || echo "0")
NOW_EPOCH=$(date +%s)
DIFF_SECONDS=$((NOW_EPOCH - LAST_EPOCH))
if [[ $DIFF_SECONDS -lt 3600 ]]; then
TIME_AGO="$((DIFF_SECONDS / 60))m ago"
elif [[ $DIFF_SECONDS -lt 86400 ]]; then
TIME_AGO="$((DIFF_SECONDS / 3600))h ago"
else
TIME_AGO="$((DIFF_SECONDS / 86400))d ago"
fi
# Adjust color based on age
if [[ $DIFF_SECONDS -gt 172800 ]]; then
# > 48h - red
STATUS_SYMBOL="\\033[38;2;255;85;85m\\033[0m"
STATUS_COLOR="\\033[38;2;255;85;85m"
elif [[ $DIFF_SECONDS -gt 86400 ]]; then
# 24-48h - yellow
STATUS_SYMBOL="\\033[38;2;241;250;140m\\033[0m"
STATUS_COLOR="\\033[38;2;241;250;140m"
fi
else
TIME_AGO="Unknown"
fi
# Extract archive statistics from borg output
# Look for lines like: "Archive name: ..." and "This archive: X.XX GB"
ARCHIVE_NAME=$(echo "$BORG_LOG" | ${pkgs.gnugrep}/bin/grep -oP "Archive name: \K.*" | tail -1)
ARCHIVE_SIZE=$(echo "$BORG_LOG" | ${pkgs.gnugrep}/bin/grep -oP "(Original size|This archive):\s+\K[0-9.]+ [KMGT]?B" | tail -1)
COMPRESSED_SIZE=$(echo "$BORG_LOG" | ${pkgs.gnugrep}/bin/grep -oP "Compressed size:\s+\K[0-9.]+ [KMGT]?B" | tail -1)
DEDUPLICATED_SIZE=$(echo "$BORG_LOG" | ${pkgs.gnugrep}/bin/grep -oP "Deduplicated size:\s+\K[0-9.]+ [KMGT]?B" | tail -1)
FILES_COUNT=$(echo "$BORG_LOG" | ${pkgs.gnugrep}/bin/grep -oP "Number of files:\s+\K[0-9]+" | tail -1)
# Display main status line
printf " %b \\033[2mLast backup\\033[0m %b%s\\033[0m" "$STATUS_SYMBOL" "$STATUS_COLOR" "$TIME_AGO"
# Add archive size if available
if [[ -n "$DEDUPLICATED_SIZE" ]]; then
printf " \\033[2m%s\\033[0m" "$DEDUPLICATED_SIZE"
elif [[ -n "$ARCHIVE_SIZE" ]]; then
printf " \\033[2m%s\\033[0m" "$ARCHIVE_SIZE"
fi
printf "\n"
# Display additional details if available
if [[ -n "$ARCHIVE_SIZE" ]] && [[ -n "$COMPRESSED_SIZE" ]] && [[ -n "$DEDUPLICATED_SIZE" ]]; then
printf " \\033[2m Original: %s Compressed: %s Dedup: %s\\033[0m\n" "$ARCHIVE_SIZE" "$COMPRESSED_SIZE" "$DEDUPLICATED_SIZE"
fi
if [[ -n "$FILES_COUNT" ]]; then
printf " \\033[2m Files: %s\\033[0m\n" "$FILES_COUNT"
fi
else
# Check for errors
if echo "$BORG_LOG" | ${pkgs.gnugrep}/bin/grep -q "ERROR"; then
STATUS_SYMBOL="\\033[38;2;255;85;85m\\033[0m"
STATUS_TEXT="FAILED"
ERROR_MSG=$(echo "$BORG_LOG" | ${pkgs.gnugrep}/bin/grep "ERROR" | tail -1 | ${pkgs.gawk}/bin/awk '{print substr($0, index($0,$2))}' | cut -c1-60)
printf " %b \\033[2mLast backup\\033[0m \\033[38;2;255;85;85m%s\\033[0m\n" "$STATUS_SYMBOL" "$STATUS_TEXT"
if [[ -n "$ERROR_MSG" ]]; then
printf " \\033[2m %s\\033[0m\n" "$ERROR_MSG"
fi
else
STATUS_SYMBOL="\\033[38;2;241;250;140m\\033[0m"
STATUS_TEXT="Unknown"
printf " %b \\033[2mLast backup\\033[0m \\033[38;2;241;250;140m%s\\033[0m\n" "$STATUS_SYMBOL" "$STATUS_TEXT"
fi
fi
fi
'';
# Combine all sections
hasDisks = cfg.showSmartStatus && (builtins.length (builtins.attrNames cfg.smartDrives) > 0);
hasStorage = cfg.showDiskUsage && (builtins.length cfg.diskUsagePaths > 0);
in ''
if [[ -n "$SSH_CONNECTION" ]] || [[ -n "$SSH_TTY" ]]; then
echo ""
printf "\\033[38;2;0;200;255m System \\033[0m\n"
${systemInfoCode}
${optionalString hasDisks ''
printf "\\033[38;2;100;150;255m Disks \\033[0m\n"
${smartStatusCode}
''}
${optionalString hasStorage ''
printf "\\033[38;2;100;150;255m Storage \\033[0m\n"
${diskUsageCode}
''}
${optionalString cfg.showContainerUpdater ''
printf "\\033[38;2;100;150;255m Containers \\033[0m\n"
${containerUpdaterStatusCode}
''}
${optionalString cfg.showBorgStatus ''
printf "\\033[38;2;100;150;255m Backup \\033[0m\n"
${borgStatusCode}
''}
echo ""
fi
''
);
# Also support bash if needed
programs.bash.loginShellInit = mkIf (!config.programs.zsh.enable) (
# Same content as zsh
programs.zsh.loginShellInit
);
};
}