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
This commit is contained in:
2026-05-29 17:19:32 +02:00
parent 6966af3229
commit 8b9c38ab48
10 changed files with 401 additions and 59 deletions
+3
View File
@@ -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
+135
View File
@@ -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 = <i>password</i>
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
}
+34 -59
View File
@@ -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
}
@@ -0,0 +1 @@
{{ template "apex-neon-lock.conf" . }}
@@ -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: []
+11
View File
@@ -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"]
+1
View File
@@ -0,0 +1 @@
{{ template "apex-neon-lock.conf" . }}
@@ -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,
+62
View File
@@ -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 = <i>username</i>
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
}
+113
View File
@@ -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 <<EOF
done. to apply on the running system (do this from a spare TTY — Ctrl+Alt+F3 — so you don't lock yourself out):
sudo systemctl restart greetd
rollback:
sudo cp /etc/greetd/greetd.conf.nwg-hello-bak /etc/greetd/greetd.conf && sudo systemctl restart greetd
EOF