From 8b9c38ab4833bda5632115d1804d3c694604bb9b Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Fri, 29 May 2026 17:19:32 +0200 Subject: [PATCH] hypr: unify lockscreen and login screen via hyprlogin Replace nwg-hello with hyprlogin as the greetd greeter. Both hyprlock and hyprlogin now share a single apex-neon theme fragment (.chezmoitemplates/apex-neon-lock.conf), giving an identical look on lock and login. - Add system/ staging tree for /etc files (not auto-applied by chezmoi) - Add system/install-greeter.sh to render templates and sudo-install to /etc - Add apex-neon shared fragment: palette, font, animations, input-field, clock/date bubbles, weather/location bubbles - Add dot_config/hypr/themes/apex-neon-lock.conf.tmpl ($HOME copy for hyprlock) - Rewrite hyprlock.conf.tmpl to source the shared fragment; move notification bubble here (hyprlock-only, requires quickshell) - Add Quickshell IpcHandler so `qs ipc call notifications count` works - Session fixed to hyprland-uwsm.desktop; greetd config targets greetd.conf --- .chezmoiignore.tmpl | 3 + .chezmoitemplates/apex-neon-lock.conf | 135 ++++++++++++++++++ dot_config/hypr/hyprlock.conf.tmpl | 93 +++++------- .../hypr/themes/apex-neon-lock.conf.tmpl | 1 + .../notifications/NotificationDaemon.qml | 7 + system/etc/greetd/greetd.conf.tmpl | 11 ++ system/etc/hyprlogin/apex-neon.conf.tmpl | 1 + .../etc/hyprlogin/hyprland-greeter.conf.tmpl | 34 +++++ system/etc/hyprlogin/hyprlogin.conf.tmpl | 62 ++++++++ system/install-greeter.sh | 113 +++++++++++++++ 10 files changed, 401 insertions(+), 59 deletions(-) create mode 100644 .chezmoitemplates/apex-neon-lock.conf create mode 100644 dot_config/hypr/themes/apex-neon-lock.conf.tmpl create mode 100644 system/etc/greetd/greetd.conf.tmpl create mode 100644 system/etc/hyprlogin/apex-neon.conf.tmpl create mode 100644 system/etc/hyprlogin/hyprland-greeter.conf.tmpl create mode 100644 system/etc/hyprlogin/hyprlogin.conf.tmpl create mode 100755 system/install-greeter.sh diff --git a/.chezmoiignore.tmpl b/.chezmoiignore.tmpl index 103290a..7cdb82a 100644 --- a/.chezmoiignore.tmpl +++ b/.chezmoiignore.tmpl @@ -37,6 +37,9 @@ pkglist/ +# system/ contains files staged for /etc — installed manually via system/install-greeter.sh +system/ + AGENTS.md GEMINI.md CLAUDE.md diff --git a/.chezmoitemplates/apex-neon-lock.conf b/.chezmoitemplates/apex-neon-lock.conf new file mode 100644 index 0000000..dd5de84 --- /dev/null +++ b/.chezmoitemplates/apex-neon-lock.conf @@ -0,0 +1,135 @@ +{{/* + Shared apex-neon look for hyprlock + hyprlogin. + + Hyprlang fragment. Sourced by: + - ~/.config/hypr/themes/apex-neon-lock.conf (hyprlock, in $HOME) + - /etc/hyprlogin/apex-neon.conf (hyprlogin, installed by system/install-greeter.sh) + + Defines: apex-neon palette, font, input-field, clock + date labels. + Greeter-only widgets (session picker, layout, username placeholder) stay in hyprlogin.conf. +*/ -}} +# apex-neon palette (mirrors ~/Dev/Themes/apex/dist/hyprland/apex-neon-colors.conf) +$base = rgb(050505) +$surface = rgb(141414) +$overlay = rgb(262626) +$muted = rgb(404040) +$text = rgb(ededed) +$love = rgb(ff0044) +$gold = rgb(ffb700) +$pine = rgb(00ff99) +$foam = rgb(00eaff) +$iris = rgb(9d00ff) + +$font = GeistMono Nerd Font + +animations { + enabled = true + bezier = linear, 1, 1, 0, 0 + animation = fadeIn, 1, 5, linear + animation = fadeOut, 1, 5, linear + animation = inputFieldDots, 1, 2, linear +} + +# input — bottom center +input-field { + monitor = + size = 360, 64 + outline_thickness = 2 + dots_size = 0.22 + dots_spacing = 0.35 + dots_center = true + + inner_color = rgba(05, 05, 05, 0.55) + outer_color = rgba(00, 234, 255, 0.85) rgba(157, 0, 255, 0.85) 45deg + check_color = rgba(0, 255, 153, 0.9) rgba(0, 234, 255, 0.9) 45deg + fail_color = rgba(255, 0, 68, 0.9) rgba(255, 136, 153, 0.9) 45deg + + font_color = rgba(237, 237, 237, 0.95) + fade_on_empty = false + rounding = 12 + + font_family = $font + placeholder_text = password + + position = 0, 120 + halign = center + valign = bottom +} + +# --- TOP-LEFT: two stacked bubbles --- +# For halign=left valign=top, position = top-left corner of the widget (relative +# to screen top-left). Shapes and labels share the same convention. + +# ── bubble 1: clock + date ── +shape { + monitor = + size = 440, 200 + color = rgba(5, 5, 5, 0.55) + rounding = 20 + border_size = 2 + border_color = rgba(0, 234, 255, 0.55) + position = 40, -50 + halign = left + valign = top + z_index = -1 +} + +label { + monitor = + text = cmd[update:1000] date +"%-H:%M" + color = rgba(237, 237, 237, 0.95) + font_size = 80 + font_family = $font + position = 100, -70 + halign = left + valign = top +} + +label { + monitor = + text = cmd[update:60000] date +"%A, %B %-d" + color = rgba(0, 234, 255, 0.85) + font_size = 20 + font_family = $font + position = 130, -180 + halign = left + valign = top +} + +# ── bubble 2: weather + location ── +shape { + monitor = + size = 360, 110 + color = rgba(5, 5, 5, 0.55) + rounding = 18 + border_size = 2 + border_color = rgba(255, 183, 0, 0.55) + position = 40, -280 + halign = left + valign = top + z_index = -1 +} + +# weather: condition + temp. wttr.in geo-locates by IP; silent on failure. +label { + monitor = + text = cmd[update:1800000] curl -fsS --max-time 3 'wttr.in/?format=%c+%t' 2>/dev/null || echo " " + color = rgba(255, 183, 0, 0.95) + font_size = 22 + font_family = $font + position = 130, -305 + halign = left + valign = top +} + +# location: trim wttr's "City, District, Country" to just the city. +label { + monitor = + text = cmd[update:1800000] curl -fsS --max-time 3 'wttr.in/?format=%l' 2>/dev/null | cut -d',' -f1 | sed 's/^./\U&/' || echo " " + color = rgba(157, 0, 255, 0.85) + font_size = 14 + font_family = $font + position = 185, -348 + halign = left + valign = top +} diff --git a/dot_config/hypr/hyprlock.conf.tmpl b/dot_config/hypr/hyprlock.conf.tmpl index d989488..d47a05b 100644 --- a/dot_config/hypr/hyprlock.conf.tmpl +++ b/dot_config/hypr/hyprlock.conf.tmpl @@ -1,70 +1,45 @@ -{{- $lock_bg := (index .chezmoi.config.data "lockscreen-wallpaper") -}} -{{- if not $lock_bg -}} - {{- $lock_bg = "~/.config/wallpaper/lockscreen/current" -}} +{{- $fallback_bg := (index .chezmoi.config.data "lockscreen-wallpaper") -}} +{{- if not $fallback_bg -}} + {{- $fallback_bg = "~/.config/wallpaper/lockscreen/current" -}} {{- end -}} +# Shared look (palette, font, input-field, clock, date) comes from the sourced fragment. +# Same fragment is installed to /etc/hyprlogin/apex-neon.conf and used by hyprlogin. +source = ~/.config/hypr/themes/apex-neon-lock.conf + general { hide_cursor = true ignore_empty_input = true } -#auth { -# just leave the defaults -#} - -################## -### BACKGROUND ### -################## +{{ range .monitors -}} +{{- $wp := index . "lockscreen-wallpaper" -}} background { - monitor = - path = {{ $lock_bg }} + monitor = {{ .name }} + path = {{ if $wp }}{{ $wp }}{{ else }}{{ $fallback_bg }}{{ end }} +} +{{ end -}} + +# --- TOP-RIGHT: notification bubble (hyprlock-only; requires quickshell) --- +shape { + monitor = + size = 130, 52 + color = rgba(5, 5, 5, 0.55) + rounding = 16 + border_size = 2 + border_color = rgba(255, 0, 68, 0.55) + position = -100, -76 + halign = right + valign = top + z_index = -1 } -############# -### INPUT ### -############# -input-field { - size = 250, 60 - outline_thickness = 2 - dots_size = 0.2 # Scale of input-field height, 0.2 - 0.8 - dots_spacing = 0.35 # Scale of dots' absolute size, 0.0 - 1.0 - dots_center = true - outer_color = rgba(0, 0, 0, 0) - inner_color = rgba(0, 0, 0, 0.2) - font_color = rgba(255, 0, 132, 0.8) - fade_on_empty = false - rounding = -1 - check_color = rgb(204, 136, 34) - placeholder_text = - hide_input = false - position = 0, -200 - halign = center - valign = center -} - -############ -### DATA ### -############ label { - monitor = - text = cmd[update:1000] echo "$(date +"%A, %B %d")" - color = rgba(242, 243, 244, 0.75) - font_size = 22 - font_family = GeistMono Nerd Font - position = 0, 300 - halign = center - valign = center -} - -############ -### TIME ### -############ -label { - monitor = - text = cmd[update:1000] echo "$(date +"%-I:%M")" - color = rgba(242, 243, 244, 0.75) - font_size = 95 - font_family = GeistMono Nerd Font - position = 0, 200 - halign = center - valign = center + monitor = + text = cmd[update:5000] n=$(qs ipc call notifications count 2>/dev/null); { [ -n "$n" ] && [ "$n" != "0" ] && printf ' %s' "$n"; } || true + color = rgba(255, 0, 68, 0.95) + font_size = 20 + font_family = GeistMono Nerd Font + position = -125, -86 + halign = right + valign = top } diff --git a/dot_config/hypr/themes/apex-neon-lock.conf.tmpl b/dot_config/hypr/themes/apex-neon-lock.conf.tmpl new file mode 100644 index 0000000..a1459c1 --- /dev/null +++ b/dot_config/hypr/themes/apex-neon-lock.conf.tmpl @@ -0,0 +1 @@ +{{ template "apex-neon-lock.conf" . }} diff --git a/dot_config/quickshell/notifications/NotificationDaemon.qml b/dot_config/quickshell/notifications/NotificationDaemon.qml index 95f8c1b..09e30a6 100644 --- a/dot_config/quickshell/notifications/NotificationDaemon.qml +++ b/dot_config/quickshell/notifications/NotificationDaemon.qml @@ -1,4 +1,5 @@ import Quickshell +import Quickshell.Io import Quickshell.Services.Notifications import Quickshell.Wayland import QtQuick @@ -12,6 +13,12 @@ Scope { property alias trackedNotifications: server.trackedNotifications property bool dnd: false + // External access (used by hyprlock notification widget via `qs ipc call notifications count`) + IpcHandler { + target: "notifications" + function count(): int { return server.trackedNotifications.values.length } + } + // Toast IDs currently showing as popups (capped to avoid overflow) readonly property int maxToasts: 4 property var toastIds: [] diff --git a/system/etc/greetd/greetd.conf.tmpl b/system/etc/greetd/greetd.conf.tmpl new file mode 100644 index 0000000..e9f986c --- /dev/null +++ b/system/etc/greetd/greetd.conf.tmpl @@ -0,0 +1,11 @@ +# /etc/greetd/config.toml — installed from chezmoi via system/install-greeter.sh +[terminal] +vt = 1 + +[default_session] +command = "start-hyprland -- --config /etc/hyprlogin/hyprland-greeter.conf" +user = "greeter" + +[commands] +reboot = ["systemctl", "reboot"] +poweroff = ["systemctl", "poweroff"] diff --git a/system/etc/hyprlogin/apex-neon.conf.tmpl b/system/etc/hyprlogin/apex-neon.conf.tmpl new file mode 100644 index 0000000..a1459c1 --- /dev/null +++ b/system/etc/hyprlogin/apex-neon.conf.tmpl @@ -0,0 +1 @@ +{{ template "apex-neon-lock.conf" . }} diff --git a/system/etc/hyprlogin/hyprland-greeter.conf.tmpl b/system/etc/hyprlogin/hyprland-greeter.conf.tmpl new file mode 100644 index 0000000..8a82485 --- /dev/null +++ b/system/etc/hyprlogin/hyprland-greeter.conf.tmpl @@ -0,0 +1,34 @@ +# Hyprland compositor config for the greeter session (run as user `greeter`) +# Installed to /etc/hyprlogin/hyprland-greeter.conf by system/install-greeter.sh. + +{{ range .monitors -}} +monitorv2 { + output = {{ .name }} + mode = {{ .width }}x{{ .height }}@{{ .refresh_rate }} + position = {{ .position }} + scale = {{ .scale }} +{{- if hasKey . "transform" }} + transform = {{ .transform }} +{{- end }} +{{- if hasKey . "vrr" }} + vrr = {{ .vrr }} +{{- end }} +} +{{ end }} +exec-once = hyprlogin + +misc { + disable_hyprland_logo = true + disable_splash_rendering = true +} + +animations { + enabled = false +} + +input { + kb_layout = us,de + kb_options = grp:alt_shift_toggle +} + +bind = ALT, Q, killactive, diff --git a/system/etc/hyprlogin/hyprlogin.conf.tmpl b/system/etc/hyprlogin/hyprlogin.conf.tmpl new file mode 100644 index 0000000..1345f4e --- /dev/null +++ b/system/etc/hyprlogin/hyprlogin.conf.tmpl @@ -0,0 +1,62 @@ +# /etc/hyprlogin/hyprlogin.conf — installed from chezmoi via system/install-greeter.sh +# Shared look (palette, font, input-field, clock, date) comes from the sourced fragment. +source = /etc/hyprlogin/apex-neon.conf + +general { + hide_cursor = false + immediate_render = true + exit_command = hyprctl dispatch exit + fail_timeout = 4000 + debug_mode = false + debug_log_path = /tmp/hyprlogin-debug.log +} + +sessions { + default_user = {{ .chezmoi.username }} + default_session = hyprland-uwsm.desktop +} + +{{ range .monitors -}} +{{- $wp := index . "lockscreen-wallpaper" -}} +{{- if $wp -}} +{{- $ext := $wp | regexFind "\\.[^.]+$" -}} +background { + monitor = {{ .name }} + path = /etc/hyprlogin/wallpaper-{{ .name }}{{ $ext }} + blur_passes = 0 +} +{{ end -}} +{{- end }} + +# greeter-only: username placeholder lives here so hyprlock doesn't carry it +input-field { + monitor = + placeholder_text_username = username + fail_text = $FAIL +} + +# session indicator (click cycles to next) +label { + monitor = + text = $GREETD_SESSION + color = rgba(157, 0, 255, 0.85) + font_size = 14 + font_family = $font + onclick = hyprlogin:session_next + position = 30, 30 + halign = left + valign = bottom +} + +# keyboard layout indicator +label { + monitor = + text = $LAYOUT + color = rgba(255, 183, 0, 0.8) + font_size = 14 + font_family = $font + onclick = hyprctl switchxkblayout all next + position = -30, 30 + halign = right + valign = bottom +} diff --git a/system/install-greeter.sh b/system/install-greeter.sh new file mode 100755 index 0000000..de3a558 --- /dev/null +++ b/system/install-greeter.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# Install hyprlogin + greetd config from chezmoi-tracked sources into /etc. +# Run manually after `chezmoi apply` when files under system/ change. +# +# Usage: ~/.local/share/chezmoi/system/install-greeter.sh [--dry-run] + +set -euo pipefail + +dry_run=0 +[[ "${1:-}" == "--dry-run" ]] && dry_run=1 + +source_dir="${CHEZMOI_SOURCE_DIR:-}" +if [[ -z "$source_dir" ]] && command -v chezmoi >/dev/null 2>&1; then + source_dir="$(chezmoi source-path 2>/dev/null || true)" +fi +[[ -z "$source_dir" ]] && source_dir="$HOME/.local/share/chezmoi" +sys_dir="$source_dir/system" + +if [[ ! -d "$sys_dir" ]]; then + echo "no system/ tree in $source_dir" >&2 + exit 1 +fi + +staging="$(mktemp -d -t greeter-staging-XXXXXX)" +trap 'rm -rf "$staging"' EXIT + +# Files to install: source.tmpl → /etc target (mode) +files=( + "etc/greetd/greetd.conf.tmpl /etc/greetd/greetd.conf 0644" + "etc/hyprlogin/apex-neon.conf.tmpl /etc/hyprlogin/apex-neon.conf 0644" + "etc/hyprlogin/hyprlogin.conf.tmpl /etc/hyprlogin/hyprlogin.conf 0644" + "etc/hyprlogin/hyprland-greeter.conf.tmpl /etc/hyprlogin/hyprland-greeter.conf 0644" +) + +render() { + local src="$1" dst="$2" + mkdir -p "$(dirname "$dst")" + chezmoi execute-template --source "$source_dir" < "$src" > "$dst" +} + +needs_install=0 +declare -a plan +for entry in "${files[@]}"; do + read -r rel target mode <<<"$entry" + src="$sys_dir/$rel" + rendered="$staging/$rel" + rendered="${rendered%.tmpl}" + render "$src" "$rendered" + if [[ ! -f "$target" ]] || ! diff -q "$rendered" "$target" >/dev/null 2>&1; then + plan+=("$rendered → $target ($mode)") + needs_install=1 + fi +done + +# Wallpapers: per-monitor, driven by chezmoi data.monitors[].lockscreen-wallpaper +# Renders a "name|path" line per monitor that has a wallpaper set. +wp_tmpl='{{ range .monitors }}{{ $wp := index . "lockscreen-wallpaper" }}{{ if $wp }}{{ .name }}|{{ $wp }} +{{ end }}{{ end }}' +declare -a wp_plan +while IFS='|' read -r mon wp; do + [[ -z "$mon" || -z "$wp" ]] && continue + wp="${wp/#\~/$HOME}" + if [[ ! -f "$wp" ]]; then + echo "warning: monitor $mon wallpaper missing: $wp" >&2 + continue + fi + ext="${wp##*.}" + target="/etc/hyprlogin/wallpaper-${mon}.${ext}" + if [[ ! -f "$target" ]] || ! cmp -s "$wp" "$target"; then + plan+=("$wp → $target (0644)") + wp_plan+=("$wp|$target") + needs_install=1 + fi +done < <(chezmoi execute-template --source "$source_dir" <<<"$wp_tmpl") + +if (( ! needs_install )); then + echo "nothing to do — /etc files already match chezmoi source" + exit 0 +fi + +echo "would install:" +printf ' %s\n' "${plan[@]}" + +if (( dry_run )); then + exit 0 +fi + +# Back up current greetd config once, the first time, for one-step rollback. +if [[ -f /etc/greetd/greetd.conf && ! -f /etc/greetd/greetd.conf.nwg-hello-bak ]]; then + echo "backing up existing /etc/greetd/greetd.conf → /etc/greetd/greetd.conf.nwg-hello-bak" + sudo cp -a /etc/greetd/greetd.conf /etc/greetd/greetd.conf.nwg-hello-bak +fi + +for entry in "${files[@]}"; do + read -r rel target mode <<<"$entry" + rendered="$staging/${rel%.tmpl}" + sudo install -Dm"$mode" "$rendered" "$target" +done + +for pair in "${wp_plan[@]:-}"; do + [[ -z "$pair" ]] && continue + IFS='|' read -r src target <<<"$pair" + sudo install -Dm0644 "$src" "$target" +done + +cat <