Switch hyprlock to QS

This commit is contained in:
David Heinemeier Hansson
2026-05-19 17:08:14 +02:00
parent 0f5044167c
commit 7ea4e1ab04
35 changed files with 784 additions and 219 deletions
-9
View File
@@ -169,12 +169,3 @@ New migration format:
Some older migrations predate these rules. Do not copy older migrations that start with shebangs, omit the leading `echo`, or hard-code `~/.local/share/omarchy`.
Migrations may use raw `pacman`, `command -v`, or direct config edits when needed for historical compatibility or one-off repair work.
Example:
```bash
echo "Disable fingerprint in hyprlock if fingerprint auth is not configured"
if omarchy-cmd-missing fprintd-list || ! fprintd-list "$USER" 2>/dev/null | grep -q "finger"; then
sed -i 's/fingerprint:enabled = .*/fingerprint:enabled = false/' ~/.config/hypr/hyprlock.conf
fi
```
+55
View File
@@ -0,0 +1,55 @@
#!/bin/bash
# omarchy:summary=Show idle, screensaver, and lock diagnostics
# omarchy:group=debug
# omarchy:args=[log-lines]
# omarchy:examples=omarchy debug idle | omarchy-debug-idle 400
lines=${1:-200}
if [[ ! $lines =~ ^[0-9]+$ ]]; then
lines=200
fi
section() {
printf '\n== %s ==\n' "$1"
}
section "Time"
date -Is
section "Idle IPC status"
omarchy-shell idle status 2>&1 | jq . 2>/dev/null || omarchy-shell idle status 2>&1 || true
section "Quickshell instances"
quickshell list -p "$OMARCHY_PATH/shell" --any-display 2>&1 || true
section "Recent idle logs"
quickshell --no-color log -p "$OMARCHY_PATH/shell" --any-display --tail "$lines" --log-times -r 'quickshell.wayland.idle_notify=true' 2>&1 \
| grep -Ei 'omarchy idle|idle_notify|screensaver|lock|error|warn|failed' || true
section "Relevant processes"
ps -eo pid=,args= \
| grep -E 'quickshell -n -p|dbus-monitor.*PrepareForSleep|org\.omarchy\.screensaver|omarchy-screensaver|(^|/| )tte( |$)' \
| grep -v grep || true
section "Hyprland screensaver clients"
hyprctl clients -j 2>/dev/null \
| jq -r '.[] | select(.class == "org.omarchy.screensaver" or .initialClass == "org.omarchy.screensaver") | [.pid,.class,.initialClass,.title,.focusHistoryID] | @tsv' || true
section "Idle inhibitors"
hyprctl clients -j 2>/dev/null \
| jq -r '.[] | select(.inhibitingIdle == true or ((.tags // []) | index("noidle"))) | [.pid,.class,.title,((.tags // []) | join(",")),.inhibitingIdle] | @tsv' || true
section "Screensaver detector"
if hyprctl clients -j 2>/dev/null | jq -e '.[] | select(.class == "org.omarchy.screensaver" or .initialClass == "org.omarchy.screensaver")' >/dev/null; then
echo "running-window"
elif pgrep -f '[o]rg.omarchy.screensaver' >/dev/null; then
echo "running-process"
elif omarchy-toggle-enabled screensaver-off; then
echo "disabled"
else
echo "stopped"
fi
section "Lock detector"
omarchy-shell lock status 2>&1 | jq . 2>/dev/null || omarchy-shell lock status 2>&1 || true
-2
View File
@@ -26,8 +26,6 @@ if [[ -n $font_name ]]; then
sed -i "s/^font=.*/font=$font_name:size=9/g" ~/.config/foot/foot.ini
fi
sed -i "s/font_family = .*/font_family = $font_name/g" ~/.config/hypr/hyprlock.conf
# fontconfig is the canonical source of truth — the omarchy shell, Qt
# apps, and anything else that resolves "monospace" all read from here.
xmlstarlet ed -L \
+1 -1
View File
@@ -2,7 +2,7 @@
# omarchy:summary=Open a config file in the user's editor and surface a toast
# omarchy:args=<path>
# omarchy:examples=omarchy launch config-editor ~/.config/hypr/hyprlock.conf
# omarchy:examples=omarchy launch config-editor ~/.config/hypr/hyprland.lua
path="${1-}"
+3 -3
View File
@@ -10,14 +10,14 @@ if [[ -z $config_file ]]; then
Usage: $0 [config_file]
Must provide a file path from the .config directory to be refreshed.
To copy ~/.local/share/omarchy/config/hypr/hyprlock.conf to ~/.config/hypr/hyprlock.conf
To copy ~/.local/share/omarchy/config/hypr/hyprland.lua to ~/.config/hypr/hyprland.lua
$0 hypr/hyprlock.conf
$0 hypr/hyprland.lua
USAGE
exit 1
fi
# Backup the destination file (with timestamp) to avoid clobbering (Ex: hyprlock.conf.bak.1753817951)
# Backup the destination file (with timestamp) to avoid clobbering (Ex: hyprland.lua.bak.1753817951)
user_config_file="${HOME}/.config/$config_file"
default_config_file="$OMARCHY_PATH/config/$config_file"
backup_config_file="$user_config_file.bak.$(date +%s)"
-5
View File
@@ -1,5 +0,0 @@
#!/bin/bash
# omarchy:summary=Overwrite the user config for hyprlock with the Omarchy default.
omarchy-refresh-config hypr/hyprlock.conf
+7 -5
View File
@@ -20,16 +20,18 @@ remove_pam_config() {
fi
}
remove_hyprlock_fingerprint_icon() {
echo "Removing fingerprint icon from hyprlock placeholder text..."
sed -i 's/placeholder_text = .*/placeholder_text = Enter Password/' ~/.config/hypr/hyprlock.conf
sed -i 's/fingerprint:enabled = .*/fingerprint:enabled = false/' ~/.config/hypr/hyprlock.conf
remove_lock_fingerprint_pam() {
if [[ -f /etc/pam.d/omarchy-lock-fingerprint ]]; then
echo "Removing lock screen fingerprint authentication..."
sudo rm -f /etc/pam.d/omarchy-lock-fingerprint
fi
}
echo -e "\e[32mRemoving fingerprint scanner from authentication.\n\e[0m"
remove_pam_config
remove_hyprlock_fingerprint_icon
remove_lock_fingerprint_pam
echo "Removing fingerprint packages..."
omarchy-pkg-drop fprintd libfprint-git
+3 -3
View File
@@ -5,6 +5,6 @@
CONFIG_DIR="$OMARCHY_PATH/shell"
if ! quickshell reload -p "$CONFIG_DIR" >/dev/null 2>&1; then
setsid quickshell -n -p "$CONFIG_DIR" >/dev/null 2>&1 &
fi
quickshell kill -p "$CONFIG_DIR" --any-display >/dev/null 2>&1 || true
sleep 0.2
setsid quickshell -n -p "$CONFIG_DIR" >/dev/null 2>&1 &
+62
View File
@@ -0,0 +1,62 @@
#!/bin/bash
# omarchy:summary=Configure Quickshell lock screen authentication
# omarchy:group=setup
# omarchy:name=lock
# omarchy:requires-sudo=true
set -e
target_user=${SUDO_USER:-}
if [[ -z $target_user && -n ${PKEXEC_UID:-} ]]; then
target_user=$(getent passwd "$PKEXEC_UID" | cut -d: -f1)
fi
target_user=${target_user:-$USER}
target_home=$(getent passwd "$target_user" | cut -d: -f6)
target_group=$(id -gn "$target_user" 2>/dev/null || echo "$target_user")
as_root() {
if (( EUID == 0 )); then
"$@"
else
sudo "$@"
fi
}
tee_root() {
local path="$1"
if (( EUID == 0 )); then
tee "$path"
else
sudo tee "$path"
fi
}
echo "Configuring lock screen password authentication..."
tee_root /etc/pam.d/omarchy-lock-password >/dev/null <<'EOF'
#%PAM-1.0
auth required pam_faillock.so preauth silent deny=10 unlock_time=120
-auth [success=2 default=ignore] pam_systemd_home.so
auth [success=1 default=bad] pam_unix.so try_first_pass nullok
auth [default=die] pam_faillock.so authfail deny=10 unlock_time=120
auth optional pam_permit.so
auth required pam_env.so
auth required pam_faillock.so authsucc
account include system-local-login
EOF
if omarchy-cmd-present fprintd-list && fprintd-list "$target_user" 2>/dev/null | grep -qi finger; then
echo "Configuring lock screen fingerprint authentication..."
tee_root /etc/pam.d/omarchy-lock-fingerprint >/dev/null <<'EOF'
#%PAM-1.0
auth required pam_fprintd.so
account include system-local-login
EOF
else
as_root rm -f /etc/pam.d/omarchy-lock-fingerprint
fi
omarchy-shell lock status >/dev/null 2>&1 && echo "Lock screen authentication configured."
+10 -8
View File
@@ -42,12 +42,16 @@ EOF
fi
}
add_hyprlock_fingerprint_icon() {
echo "Adding fingerprint icon to hyprlock placeholder text..."
sed -i 's/placeholder_text = .*/placeholder_text = <span> Enter Password 󰈷 <\/span>/' ~/.config/hypr/hyprlock.conf
sed -i 's/fingerprint:enabled = .*/fingerprint:enabled = true/' ~/.config/hypr/hyprlock.conf
setup_lock_fingerprint_pam() {
echo "Configuring lock screen for fingerprint authentication..."
sudo tee /etc/pam.d/omarchy-lock-fingerprint >/dev/null <<'EOF'
#%PAM-1.0
auth required pam_fprintd.so
account include system-local-login
EOF
}
echo -e "\e[32mSetting up fingerprint scanner for authentication.\n\e[0m"
# Install required packages
@@ -71,9 +75,6 @@ fi
# Configure PAM
setup_pam_config
# Add fingerprint icon to hyprlock placeholder text
add_hyprlock_fingerprint_icon
# Enroll first fingerprint
echo -e "\e[32m\nLet's setup your right index finger as the first fingerprint.\e[0m"
echo -e "Keep moving the finger around on sensor until the process completes.\n"
@@ -84,8 +85,9 @@ if sudo fprintd-enroll "$USER"; then
# Verify
echo -e "\nNow let's verify that it's working correctly.\n"
if fprintd-verify; then
setup_lock_fingerprint_pam
echo -e "\e[32m\nPerfect! Fingerprint authentication is now configured.\e[0m"
echo "You can use your fingerprint for sudo, polkit, and lock screen (Super + Escape)."
echo "You can use your fingerprint for sudo, polkit, and lock screen (Super + Ctrl + L)."
else
echo -e "\e[31m\nVerification failed. You may want to try enrolling again.\e[0m"
fi
+1 -2
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# omarchy:summary=Set Hyprland, Hyprlock, and Walker corners to sharp or round (the shell mirrors Hyprland)
# omarchy:summary=Set Hyprland and Walker corners to sharp or round (the shell mirrors Hyprland)
# omarchy:args=<sharp|round>
# omarchy:examples=omarchy style corners round | omarchy style corners sharp
@@ -14,7 +14,6 @@ fi
# state file is written. The old omarchy-style-corners-quickshell shim is
# kept as a no-op so external callers don't error.
omarchy-style-corners-hyprland "$1"
omarchy-style-corners-hyprlock "$1"
omarchy-style-corners-walker "$1"
case $1 in
-35
View File
@@ -1,35 +0,0 @@
#!/bin/bash
# omarchy:summary=Set or toggle rounded Hyprlock input field corners
# omarchy:args=[toggle|sharp|round]
# omarchy:examples=omarchy style corners hyprlock toggle | omarchy style corners hyprlock round | omarchy style corners hyprlock sharp
TOGGLES_CONFIG="$HOME/.local/state/omarchy/toggles/hyprlock.conf"
set_rounding() {
local radius="$1"
mkdir -p "$(dirname "$TOGGLES_CONFIG")"
cat >"$TOGGLES_CONFIG" <<EOF
input-field {
rounding = $radius
}
EOF
}
case "${1:-toggle}" in
round) set_rounding 6 ;;
sharp) set_rounding 0 ;;
toggle)
if [[ -f $TOGGLES_CONFIG ]] && grep -q '^ rounding = 6$' "$TOGGLES_CONFIG"; then
set_rounding 0
else
set_rounding 6
fi
;;
*)
echo "Usage: omarchy-style-corners-hyprlock [toggle|sharp|round]"
exit 1
;;
esac
+3 -8
View File
@@ -5,12 +5,7 @@
# omarchy:name=lock
# omarchy:examples=omarchy system lock
if ! pidof hyprlock >/dev/null; then
(
hyprlock
omarchy-system-wake
) &
fi
omarchy-shell lock lock >/dev/null
# Set keyboard layout to default (first layout)
hyprctl switchxkblayout all 0 > /dev/null 2>&1
@@ -21,12 +16,12 @@ if pgrep -x "1password" >/dev/null; then
fi
# Avoid running screensaver when locked
pkill -f org.omarchy.screensaver
pkill -f '[o]rg.omarchy.screensaver'
if [[ ${OMARCHY_LOCK_ONLY:-false} != "true" ]]; then
(
sleep 3
pidof hyprlock >/dev/null || exit 0
[[ $(omarchy-shell lock isLocked 2>/dev/null) == "true" ]] || exit 0
omarchy-brightness-keyboard off
omarchy-brightness-display off
) &
-45
View File
@@ -1,45 +0,0 @@
source = ~/.config/omarchy/current/theme/hyprlock.conf
general {
ignore_empty_input = true
}
background {
monitor =
color = $color
path = ~/.config/omarchy/current/background
blur_passes = 3
}
animations {
enabled = false
}
input-field {
monitor =
size = 650, 100
position = 0, 0
halign = center
valign = center
inner_color = $inner_color
outer_color = $outer_color
outline_thickness = 4
font_family = JetBrainsMono Nerd Font
font_color = $font_color
placeholder_text = Enter Password
check_color = $check_color
fail_text = <i>$FAIL ($ATTEMPTS)</i>
rounding = 0
shadow_passes = 0
fade_on_empty = false
}
auth {
fingerprint:enabled = false
}
source = ~/.local/state/omarchy/toggles/hyprlock.conf
+2 -2
View File
@@ -136,7 +136,7 @@ Run `omarchy --help` for the full list. The most common groups:
├── looknfeel.conf # Appearance (gaps, borders, animations)
├── envs.conf # Environment variables
├── autostart.conf # Startup applications
├── hyprlock.conf # Lock screen appearance and auth
├── shell.json # Omarchy shell, bar, idle, and lock settings
└── hyprsunset.conf # Night light / blue light filter
```
@@ -343,7 +343,7 @@ omarchy refresh <app>
# Refresh specific config file
# config-file path is relative to ~/.config/
# eg. `omarchy refresh config hypr/hyprlock.conf` will refresh ~/.config/hypr/hyprlock.conf
# eg. `omarchy refresh config hypr/hyprland.lua` will refresh ~/.config/hypr/hyprland.lua
omarchy refresh config <config-file>
# Full reinstall of configs (nuclear option)
-2
View File
@@ -162,7 +162,6 @@
"setup.security.fingerprint": {"icon":"󰈷","label":"Fingerprint","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fingerprint"},
"setup.security.fido2": {"icon":"","label":"Fido2","keywords":"key","action":"omarchy-launch-floating-terminal-with-presentation omarchy-setup-security-fido2"},
"setup.config.hyprland": {"icon":"","label":"Hyprland","action":"omarchy-launch-config-editor \"$HOME/.config/hypr/hyprland.lua\""},
"setup.config.hyprlock": {"icon":"","label":"Hyprlock","keywords":"lock","action":"omarchy-launch-config-editor ~/.config/hypr/hyprlock.conf"},
"setup.config.hyprsunset": {"icon":"","label":"Hyprsunset","keywords":"night light","action":"omarchy-launch-config-editor ~/.config/hypr/hyprsunset.conf && omarchy-restart-hyprsunset"},
"setup.config.walker": {"icon":"󰌧","label":"Walker","keywords":"launcher","action":"omarchy-launch-config-editor ~/.config/walker/config.toml && omarchy-restart-walker"},
"setup.config.bar": {"icon":"󰍜","label":"Bar","keywords":"quickshell shell config settings widgets position","action":"omarchy-launch-bar-settings"},
@@ -313,7 +312,6 @@
"update.process.walker": {"icon":"󰌧","label":"Walker","keywords":"restart launcher","action":"omarchy-restart-walker"},
"update.process.shell": {"icon":"󰍜","label":"Shell","keywords":"restart bar quickshell omarchy-shell","action":"omarchy-restart-shell"},
"update.config.hyprland": {"icon":"","label":"Hyprland","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-hyprland"},
"update.config.hyprlock": {"icon":"","label":"Hyprlock","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-hyprlock"},
"update.config.hyprsunset": {"icon":"","label":"Hyprsunset","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-hyprsunset"},
"update.config.plymouth": {"icon":"󱣴","label":"Plymouth","keywords":"refresh","action":"omarchy-launch-floating-terminal-with-presentation omarchy-refresh-plymouth"},
"update.config.osd": {"icon":"","label":"OSD","keywords":"refresh","action":"omarchy-restart-shell"},
-5
View File
@@ -1,5 +0,0 @@
$color = rgba({{ background_rgb }}, 1.0)
$inner_color = rgba({{ background_rgb }}, 0.8)
$outer_color = rgba({{ foreground_rgb }}, 1.0)
$font_color = rgba({{ foreground_rgb }}, 1.0)
$check_color = rgba({{ accent_rgb }}, 1.0)
+1
View File
@@ -6,6 +6,7 @@ run_logged $OMARCHY_INSTALL/config/gpg.sh
run_logged $OMARCHY_INSTALL/config/timezones.sh
run_logged $OMARCHY_INSTALL/config/increase-sudo-tries.sh
run_logged $OMARCHY_INSTALL/config/increase-lockout-limit.sh
run_logged $OMARCHY_INSTALL/config/lockscreen-pam.sh
run_logged $OMARCHY_INSTALL/config/ssh-flakiness.sh
run_logged $OMARCHY_INSTALL/config/increase-file-watchers.sh
run_logged $OMARCHY_INSTALL/config/increase-fd-limit.sh
+1 -1
View File
@@ -2,5 +2,5 @@
echo "Defaults passwd_tries=10" | sudo tee /etc/sudoers.d/passwd-tries
sudo chmod 440 /etc/sudoers.d/passwd-tries
# Set for hyprlock too
# Set for the lock screen too
sudo sed -i 's/^# *deny = .*/deny = 10/' /etc/security/faillock.conf
+1
View File
@@ -0,0 +1 @@
omarchy-setup-lock
-1
View File
@@ -1,4 +1,3 @@
# Make sure toggles are available
mkdir -p ~/.local/state/omarchy/toggles
touch ~/.local/state/omarchy/toggles/hyprlock.conf
touch ~/.local/state/omarchy/toggles/walker.css
-1
View File
@@ -52,7 +52,6 @@ gvfs-smb
hyprland
hyprland-guiutils
hyprland-preview-share-picker
hyprlock
hyprpicker
hyprsunset
imagemagick
-3
View File
@@ -1,3 +0,0 @@
echo "Update Hyprlock with better placeholder position and show all fail text"
omarchy-refresh-hyprlock
-12
View File
@@ -1,12 +0,0 @@
echo "Update hyprlock font to match current system font"
font_name=$(omarchy-font-current)
if [[ -n $font_name ]]; then
cp ~/.config/hypr/hyprlock.conf ~/.config/hypr/hyprlock.conf.bak.$(date +%s)
echo "Found font '$font_name', updating hyprlock"
sed -i "s/font_family = .*/font_family = $font_name/g" ~/.config/hypr/hyprlock.conf
else
echo "No font found, skipping migration"
fi
-12
View File
@@ -1,12 +0,0 @@
echo "Update hyprlock placeholder text based on fingerprint setup status"
cp ~/.config/hypr/hyprlock.conf ~/.config/hypr/hyprlock.conf.bak.$(date +%s)
# Check if fprintd is installed and has enrolled fingerprints
if command -v fprintd-list &>/dev/null && fprintd-list "$USER" 2>/dev/null | grep -q "Fingerprints for user"; then
echo "Fingerprint detected, updating placeholder text with fingerprint icon"
sed -i 's/placeholder_text = .*/placeholder_text = <span> Enter Password 󰈷 <\/span>/' ~/.config/hypr/hyprlock.conf
else
echo "No fingerprint enrolled, updating placeholder text without fingerprint icon"
sed -i 's/placeholder_text = .*/placeholder_text = Enter Password/' ~/.config/hypr/hyprlock.conf
fi
-5
View File
@@ -1,5 +0,0 @@
echo "Ignore empty passwords on lock screen"
if ! grep -q "ignore_empty_input" ~/.config/hypr/hyprlock.conf; then
sed -i "/source =/a\ \ngeneral {\n ignore_empty_input = true\n}" ~/.config/hypr/hyprlock.conf
fi
-5
View File
@@ -1,5 +0,0 @@
echo "Disable fingerprint in hyprlock if fingerprint auth is not configured"
if omarchy-cmd-missing fprintd-list || ! fprintd-list "$USER" 2>/dev/null | grep -q "finger"; then
sed -i 's/fingerprint:enabled = .*/fingerprint:enabled = false/' ~/.config/hypr/hyprlock.conf
fi
+9
View File
@@ -0,0 +1,9 @@
echo "Switch lock screen authentication to Quickshell"
omarchy-setup-lock
rm -f ~/.local/state/omarchy/toggles/hyprlock.conf ~/.local/state/omarchy/toggles/lock.conf
rm -f ~/.config/hypr/hyprlock.conf ~/.config/omarchy/current/theme/hyprlock.conf
omarchy-pkg-drop hyprlock
+9
View File
@@ -19,6 +19,7 @@ User-installed plugins live alongside these conceptually but on disk under
| Omarchy menu | `omarchy.menu` | `menu` | `menu/Menu.qml` |
| Notifications | `omarchy.notifications` | `service` | `notifications/Service.qml` |
| Idle monitor | `omarchy.idle` | `service` | `idle/Service.qml` |
| Lock screen | `omarchy.lock` | `service` | `lock/Service.qml` |
| OSD | `omarchy.osd` | `panel` | `osd/Osd.qml` |
| Polkit agent | `omarchy.polkit` | `service` | `polkit/PolkitAgent.qml` |
@@ -69,6 +70,14 @@ clears it without writing a selection.
The plugin has `keepLoaded: true` so the layer-shell window survives
between summons within a single shell session.
## Lock screen
Session-lock surface using Quickshell's native `WlSessionLock` and two
separate PAM services: `omarchy-lock-password` for password auth and,
only when fingerprints are enrolled, `omarchy-lock-fingerprint` for
fingerprint auth. It mirrors the previous lock screen field dimensions,
colors, blurred wallpaper, placeholder, and corner toggle.
## Polkit agent
Theme-aware authentication dialog for privileged actions. It uses
+131 -32
View File
@@ -21,6 +21,11 @@ Item {
property bool idledThisCycle: false
property bool screensaverStartedThisCycle: false
property string lastEvent: "starting"
property string lastEventAt: ""
property string lastScreensaverState: "unknown"
property string lastScreensaverCheckAt: ""
property int screensaverCheckCount: 0
PersistentProperties {
id: persisted
@@ -34,8 +39,23 @@ Item {
return Math.floor(n)
}
function runProcess(process, command) {
if (process.running) return false
function nowIso() {
return new Date().toISOString()
}
function logEvent(event, details) {
var suffix = details === undefined || details === null || details === "" ? "" : ": " + String(details)
root.lastEventAt = nowIso()
root.lastEvent = event + suffix
console.log("omarchy idle " + root.lastEventAt + " " + root.lastEvent)
}
function runProcess(process, label, command) {
if (process.running) {
logEvent("process-skip", label + " already running")
return false
}
logEvent("process-start", label + " " + command)
process.command = ["bash", "-lc", command]
process.running = true
return true
@@ -43,21 +63,27 @@ Item {
function launchScreensaver() {
root.screensaverStartedThisCycle = true
runProcess(screensaverProcess, "pidof hyprlock >/dev/null || omarchy-launch-screensaver")
screensaverResumePollTimer.restart()
runProcess(screensaverProcess, "screensaver", "[[ $(omarchy-shell lock isLocked 2>/dev/null) == \"true\" ]] || omarchy-launch-screensaver")
}
function lockSystem() {
function lockSystem(reason) {
logEvent("lock-system", reason || "requested")
screensaverTimer.stop()
lockTimer.stop()
screensaverResumePollTimer.stop()
root.idledThisCycle = false
root.screensaverStartedThisCycle = false
runProcess(lockProcess, "omarchy-system-lock")
runProcess(lockProcess, "lock", "omarchy-system-lock")
}
function startIdleCycle() {
if (root.idledThisCycle) return
if (root.idledThisCycle) {
logEvent("idle-cycle-already-running")
return
}
logEvent("idle-cycle-start", "screensaver=" + root.screensaverTimeoutSeconds + " lock=" + root.lockTimeoutSeconds)
root.idledThisCycle = true
root.screensaverStartedThisCycle = false
screensaverResumePollTimer.stop()
@@ -65,16 +91,17 @@ Item {
if (root.screensaverDelaySeconds === 0) launchScreensaver()
else screensaverTimer.restart()
if (root.lockDelaySeconds === 0) lockSystem()
if (root.lockDelaySeconds === 0) lockSystem("lock-timeout-immediate")
else lockTimer.restart()
}
function cancelIdleCycle() {
function cancelIdleCycle(reason) {
logEvent("idle-cycle-cancel", reason || "requested")
screensaverTimer.stop()
lockTimer.stop()
screensaverResumePollTimer.stop()
if (root.idledThisCycle) runProcess(wakeProcess, "omarchy-system-wake")
if (root.idledThisCycle) runProcess(wakeProcess, "wake", "omarchy-system-wake")
root.idledThisCycle = false
root.screensaverStartedThisCycle = false
@@ -83,7 +110,9 @@ Item {
function checkScreensaverAfterActiveSignal() {
if (!root.idledThisCycle || !root.screensaverStartedThisCycle) return
if (screensaverCheckProcess.running) return
screensaverCheckProcess.command = ["bash", "-lc", "pgrep -f org.omarchy.screensaver >/dev/null && echo running || echo stopped"]
root.lastScreensaverState = "checking"
root.lastScreensaverCheckAt = nowIso()
screensaverCheckProcess.command = ["bash", "-lc", "if hyprctl clients -j 2>/dev/null | jq -e '.[] | select(.class == \"org.omarchy.screensaver\" or .initialClass == \"org.omarchy.screensaver\")' >/dev/null; then echo running-window; elif pgrep -f '[o]rg.omarchy.screensaver' >/dev/null; then echo running-process; elif omarchy-toggle-enabled screensaver-off; then echo disabled; else echo stopped; fi"]
screensaverCheckProcess.running = true
}
@@ -94,14 +123,16 @@ Item {
// the lock timer running while the screensaver is still alive so the lock
// deadline remains `idle.lock` seconds from the original user idle time.
if (root.screensaverStartedThisCycle) {
logEvent("idle-monitor-active", "screensaver cycle remains armed")
screensaverResumePollTimer.restart()
return
}
cancelIdleCycle()
cancelIdleCycle("activity")
}
function handleIdleChanged() {
logEvent("idle-monitor", idleMonitor.isIdle ? "idle" : "active")
if (!root.idleEnabled) return
if (idleMonitor.isIdle) startIdleCycle()
@@ -113,9 +144,31 @@ Item {
enabled: root.idleEnabled,
idle: idleMonitor.isIdle,
inIdleCycle: root.idledThisCycle,
screensaverStarted: root.screensaverStartedThisCycle,
sleepMonitor: sleepMonitorProcess.running,
screensaver: root.screensaverTimeoutSeconds,
lock: root.lockTimeoutSeconds
lock: root.lockTimeoutSeconds,
screensaverDelay: root.screensaverDelaySeconds,
lockDelay: root.lockDelaySeconds,
timers: {
screensaver: screensaverTimer.running,
lock: lockTimer.running,
poll: screensaverResumePollTimer.running,
sleepMonitorRestart: sleepMonitorRestartTimer.running
},
processes: {
screensaver: screensaverProcess.running,
lock: lockProcess.running,
wake: wakeProcess.running,
check: screensaverCheckProcess.running,
sleepLock: sleepLockProcess.running,
sleepWake: sleepWakeProcess.running
},
lastEvent: root.lastEvent,
lastEventAt: root.lastEventAt,
lastScreensaverState: root.lastScreensaverState,
lastScreensaverCheckAt: root.lastScreensaverCheckAt,
screensaverCheckCount: root.screensaverCheckCount
})
}
@@ -123,10 +176,10 @@ Item {
if (!root.idleEnabled) return
if (preparing) {
cancelIdleCycle()
runProcess(sleepLockProcess, "OMARCHY_LOCK_ONLY=true omarchy-system-lock")
cancelIdleCycle("sleep-preparing")
runProcess(sleepLockProcess, "sleep-lock", "OMARCHY_LOCK_ONLY=true omarchy-system-lock")
} else {
runProcess(sleepWakeProcess, "sleep 1 && omarchy-system-wake")
runProcess(sleepWakeProcess, "sleep-wake", "sleep 1 && omarchy-system-wake")
}
}
@@ -135,7 +188,8 @@ Item {
if (persisted.idleEnabled === enabled) return enabled ? "enabled" : "disabled"
persisted.idleEnabled = enabled
if (!enabled) cancelIdleCycle()
logEvent("idle-enabled", enabled ? "enabled" : "disabled")
if (!enabled) cancelIdleCycle("disabled")
else Qt.callLater(root.handleIdleChanged)
return enabled ? "enabled" : "disabled"
@@ -160,7 +214,7 @@ Item {
id: lockTimer
interval: root.lockDelaySeconds * 1000
repeat: false
onTriggered: if (root.idleEnabled && root.idledThisCycle) root.lockSystem()
onTriggered: if (root.idleEnabled && root.idledThisCycle) root.lockSystem("lock-timeout")
}
Timer {
@@ -170,22 +224,52 @@ Item {
onTriggered: root.checkScreensaverAfterActiveSignal()
}
Process { id: screensaverProcess }
Process { id: lockProcess }
Process { id: wakeProcess }
Process { id: sleepLockProcess }
Process { id: sleepWakeProcess }
Process {
id: screensaverProcess
onExited: function(exitCode, exitStatus) { root.logEvent("process-exit", "screensaver exitCode=" + exitCode + " status=" + exitStatus) }
}
Process {
id: lockProcess
onExited: function(exitCode, exitStatus) { root.logEvent("process-exit", "lock exitCode=" + exitCode + " status=" + exitStatus) }
}
Process {
id: wakeProcess
onExited: function(exitCode, exitStatus) { root.logEvent("process-exit", "wake exitCode=" + exitCode + " status=" + exitStatus) }
}
Process {
id: sleepLockProcess
onExited: function(exitCode, exitStatus) { root.logEvent("process-exit", "sleep-lock exitCode=" + exitCode + " status=" + exitStatus) }
}
Process {
id: sleepWakeProcess
onExited: function(exitCode, exitStatus) { root.logEvent("process-exit", "sleep-wake exitCode=" + exitCode + " status=" + exitStatus) }
}
Process {
id: screensaverCheckProcess
stdout: StdioCollector {
id: screensaverCheckStdout
waitForEnd: true
stdout: SplitParser {
onRead: function(line) {
root.lastScreensaverState = String(line || "").trim()
root.lastScreensaverCheckAt = root.nowIso()
root.screensaverCheckCount++
root.logEvent("screensaver-check", root.lastScreensaverState)
}
}
onExited: {
if (!root.idledThisCycle || !root.screensaverStartedThisCycle) return
if (String(screensaverCheckStdout.text || "").trim() === "running") return
root.cancelIdleCycle()
onExited: function(exitCode, exitStatus) {
root.logEvent("process-exit", "screensaver-check exitCode=" + exitCode + " status=" + exitStatus + " state=" + root.lastScreensaverState)
if (!root.idleEnabled || !root.idledThisCycle || !root.screensaverStartedThisCycle) return
var state = String(root.lastScreensaverState || "").trim()
if (state.indexOf("running") === 0) return
if (state === "disabled") {
screensaverResumePollTimer.stop()
return
}
// If the screensaver disappears while we're still in the idle cycle,
// treat that as the user returning and require authentication before
// showing the desktop again.
root.lockSystem("screensaver-stopped state=" + state)
}
}
@@ -200,17 +284,28 @@ Item {
else if (text === "boolean false") root.handleSleepPreparing(false)
}
}
onExited: sleepMonitorRestartTimer.restart()
onExited: {
root.logEvent("sleep-monitor-exit", "restarting")
sleepMonitorRestartTimer.restart()
}
}
Timer {
id: sleepMonitorRestartTimer
interval: 5000
repeat: false
onTriggered: if (!sleepMonitorProcess.running) sleepMonitorProcess.running = true
onTriggered: {
if (!sleepMonitorProcess.running) {
root.logEvent("sleep-monitor-restart")
sleepMonitorProcess.running = true
}
}
}
Component.onCompleted: Qt.callLater(root.handleIdleChanged)
Component.onCompleted: {
logEvent("service-ready")
Qt.callLater(root.handleIdleChanged)
}
IpcHandler {
target: "idle"
@@ -219,6 +314,10 @@ Item {
return root.statusJson()
}
function debug(): string {
return root.statusJson()
}
function enable(): string {
return root.setIdleEnabled(true)
}
+152
View File
@@ -0,0 +1,152 @@
import QtQuick
import QtQuick.Effects
import qs.Commons
Item {
id: root
property string backgroundPath: ""
property int backgroundVersion: 0
property bool fingerprintConfigured: false
property bool authenticatingPassword: false
property string failureMessage: ""
property int failedAttempts: 0
property bool inputEnabled: true
property real scaleFactor: 1
property bool hasTyped: false
readonly property string fingerprintGlyph: "\uDB80\uDE37"
readonly property string placeholderText: fingerprintConfigured ? "Enter Password " + fingerprintGlyph : "Enter Password"
readonly property real effectiveScale: Math.max(1, scaleFactor)
readonly property int fieldWidth: Math.round(650 / effectiveScale)
readonly property int fieldHeight: Math.round(100 / effectiveScale)
readonly property int outlineThickness: Math.max(1, Math.round(4 / effectiveScale))
readonly property int fieldFontSize: Style.font.heading
signal submitPassword(string password)
function withAlpha(color, alpha) {
return Qt.rgba(color.r, color.g, color.b, alpha)
}
function fileUrl(path) {
if (!path) return ""
var encoded = String(path).split("/").map(encodeURIComponent).join("/")
return "file://" + encoded + "?v=" + backgroundVersion
}
function forcePasswordFocus() {
passwordInput.forceActiveFocus()
}
function clearPassword() {
passwordInput.text = ""
hasTyped = false
}
onInputEnabledChanged: {
hasTyped = false
if (inputEnabled) Qt.callLater(forcePasswordFocus)
}
Component.onCompleted: {
hasTyped = false
if (inputEnabled) Qt.callLater(forcePasswordFocus)
}
Rectangle {
anchors.fill: parent
color: Color.background
Image {
id: wallpaper
anchors.fill: parent
source: root.fileUrl(root.backgroundPath)
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false
sourceSize.width: width
sourceSize.height: height
}
MultiEffect {
anchors.fill: wallpaper
source: wallpaper
blurEnabled: wallpaper.status === Image.Ready
blur: 1.0
blurMax: 64
blurMultiplier: 1.0
}
MouseArea {
anchors.fill: parent
onClicked: root.forcePasswordFocus()
}
Rectangle {
id: inputField
width: root.fieldWidth
height: root.fieldHeight
anchors.centerIn: parent
color: root.withAlpha(Color.background, 0.8)
border.color: root.failureMessage.length > 0 ? Color.urgent : (root.authenticatingPassword ? Color.accent : Color.foreground)
border.width: root.outlineThickness
radius: Style.cornerRadius
clip: true
TextInput {
id: passwordInput
anchors.fill: parent
anchors.leftMargin: root.outlineThickness + 18
anchors.rightMargin: root.outlineThickness + 18
verticalAlignment: TextInput.AlignVCenter
horizontalAlignment: TextInput.AlignHCenter
activeFocusOnPress: true
clip: true
enabled: root.inputEnabled && !root.authenticatingPassword
readOnly: root.authenticatingPassword
echoMode: TextInput.Password
passwordCharacter: "\u2022"
passwordMaskDelay: 0
color: Color.foreground
selectionColor: root.withAlpha(Color.accent, 0.45)
selectedTextColor: Color.foreground
font.family: "monospace"
font.pixelSize: root.fieldFontSize
cursorVisible: activeFocus && !root.authenticatingPassword && root.hasTyped
onTextChanged: {
if (text.length > 0) root.hasTyped = true
if (text.length > 0 && root.failureMessage.length > 0) root.failureMessage = ""
}
onAccepted: {
var submitted = text
text = ""
root.hasTyped = false
if (submitted.length > 0) root.submitPassword(submitted)
}
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape || (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_U)) {
text = ""
root.hasTyped = false
event.accepted = true
}
}
}
Text {
anchors.fill: passwordInput
text: root.failureMessage.length > 0 ? root.failureMessage : root.placeholderText
visible: passwordInput.text.length === 0
color: root.failureMessage.length > 0 ? Color.urgent : Color.foreground
font.family: "monospace"
font.pixelSize: root.fieldFontSize
font.italic: root.failureMessage.length > 0
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
}
}
}
+318
View File
@@ -0,0 +1,318 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pam
import Quickshell.Wayland
import qs.Commons
Item {
id: root
property var shell: null
property string omarchyPath: ""
readonly property string home: Quickshell.env("HOME")
readonly property string userName: Quickshell.env("USER") || Quickshell.env("LOGNAME")
readonly property string currentBackgroundLink: home + "/.config/omarchy/current/background"
property bool lockRequested: false
property bool authenticatingPassword: false
property bool fingerprintAuthenticating: false
property bool passwordPamConfigured: false
property bool fingerprintConfigured: false
property bool previewVisible: false
property string pendingPassword: ""
property string failureMessage: ""
property int failedAttempts: 0
property string backgroundPath: ""
property int backgroundVersion: 0
readonly property bool locked: lockRequested || sessionLock.locked || sessionLock.secure
readonly property bool authenticating: authenticatingPassword || fingerprintAuthenticating
function refreshBackground() {
if (!readlinkProc.running) readlinkProc.running = true
}
function refreshFingerprintStatus() {
if (!fingerprintCheckProc.running) fingerprintCheckProc.running = true
}
function resetAuthenticationState() {
pendingPassword = ""
failureMessage = ""
failedAttempts = 0
authenticatingPassword = false
fingerprintAuthenticating = false
fingerprintRetryTimer.stop()
if (passwordPam.active) passwordPam.abort()
if (fingerprintPam.active) fingerprintPam.abort()
}
function beginLock() {
if (!passwordPamConfigured) return false
resetAuthenticationState()
refreshBackground()
refreshFingerprintStatus()
lockRequested = true
sessionLock.locked = true
return true
}
function finishUnlock() {
if (!root.locked && !lockRequested) return
lockRequested = false
resetAuthenticationState()
sessionLock.locked = false
runWake()
}
function runWake() {
if (!wakeProcess.running) wakeProcess.running = true
}
function submitPassword(value) {
var password = String(value || "")
if (!lockRequested || authenticatingPassword || password.length === 0) return
pendingPassword = password
failureMessage = ""
authenticatingPassword = true
if (!passwordPam.start()) {
handlePasswordFailure()
return
}
Qt.callLater(respondToPasswordPrompt)
}
function respondToPasswordPrompt() {
if (!authenticatingPassword || !passwordPam.active || !passwordPam.responseRequired) return
passwordPam.respond(pendingPassword)
}
function handlePasswordFailure() {
if (!lockRequested) return
authenticatingPassword = false
pendingPassword = ""
failedAttempts += 1
failureMessage = "Authentication failed (" + failedAttempts + ")"
}
function startFingerprint() {
if (!lockRequested || !sessionLock.secure || !fingerprintConfigured) return
if (fingerprintPam.active || fingerprintAuthenticating) return
fingerprintAuthenticating = true
if (!fingerprintPam.start()) {
fingerprintAuthenticating = false
}
}
function handleFingerprintFinished(result) {
fingerprintAuthenticating = false
if (!lockRequested) return
if (result === PamResult.Success) {
finishUnlock()
} else if (fingerprintConfigured) {
fingerprintRetryTimer.restart()
}
}
WlSessionLock {
id: sessionLock
locked: false
onSecureStateChanged: {
if (secure) root.startFingerprint()
}
onLockStateChanged: {
if (!locked && root.lockRequested) {
root.lockRequested = false
root.resetAuthenticationState()
root.runWake()
}
}
WlSessionLockSurface {
id: lockSurface
color: Color.background
LockView {
id: lockView
anchors.fill: parent
backgroundPath: root.backgroundPath
backgroundVersion: root.backgroundVersion
fingerprintConfigured: root.fingerprintConfigured
authenticatingPassword: root.authenticatingPassword
failureMessage: root.failureMessage
failedAttempts: root.failedAttempts
inputEnabled: root.lockRequested
scaleFactor: lockSurface.screen && lockSurface.screen.devicePixelRatio > 0 ? lockSurface.screen.devicePixelRatio : 1
onSubmitPassword: function(password) { root.submitPassword(password) }
}
}
}
PanelWindow {
id: previewWindow
visible: root.previewVisible
anchors { top: true; bottom: true; left: true; right: true }
color: "transparent"
WlrLayershell.namespace: "omarchy-lock-preview"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
exclusionMode: ExclusionMode.Ignore
LockView {
anchors.fill: parent
backgroundPath: root.backgroundPath
backgroundVersion: root.backgroundVersion
fingerprintConfigured: root.fingerprintConfigured
authenticatingPassword: false
failureMessage: ""
failedAttempts: 0
inputEnabled: false
scaleFactor: previewWindow.screen && previewWindow.screen.devicePixelRatio > 0 ? previewWindow.screen.devicePixelRatio : 1
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: root.previewVisible = false
}
}
PamContext {
id: passwordPam
config: "omarchy-lock-password"
user: root.userName
onResponseRequiredChanged: root.respondToPasswordPrompt()
onPamMessage: root.respondToPasswordPrompt()
onCompleted: function(result) {
root.authenticatingPassword = false
root.pendingPassword = ""
if (!root.lockRequested) return
if (result === PamResult.Success) root.finishUnlock()
else root.handlePasswordFailure()
}
onError: function(error) {
root.handlePasswordFailure()
}
}
PamContext {
id: fingerprintPam
config: "omarchy-lock-fingerprint"
user: root.userName
onCompleted: function(result) {
root.handleFingerprintFinished(result)
}
onError: function(error) {
root.fingerprintAuthenticating = false
if (root.lockRequested && root.fingerprintConfigured) fingerprintRetryTimer.restart()
}
}
Timer {
id: fingerprintRetryTimer
interval: 250
repeat: false
onTriggered: root.startFingerprint()
}
Process {
id: readlinkProc
command: ["readlink", "-f", root.currentBackgroundLink]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: {
var next = String(text || "").trim()
if (next !== root.backgroundPath) {
root.backgroundPath = next
root.backgroundVersion += 1
}
}
}
}
Process {
id: fingerprintCheckProc
command: ["bash", "-lc", "if [[ -f /etc/pam.d/omarchy-lock-fingerprint ]] && command -v fprintd-list >/dev/null 2>&1 && fprintd-list \"$USER\" 2>/dev/null | grep -qi finger; then echo yes; else echo no; fi"]
stdout: StdioCollector { id: fingerprintCheckStdout; waitForEnd: true }
onExited: {
root.fingerprintConfigured = String(fingerprintCheckStdout.text || "").trim() === "yes"
if (root.lockRequested && root.fingerprintConfigured) root.startFingerprint()
else if (!root.fingerprintConfigured && fingerprintPam.active) fingerprintPam.abort()
}
}
Process {
id: wakeProcess
command: ["bash", "-lc", "omarchy-system-wake"]
}
FileView {
path: "/etc/pam.d/omarchy-lock-password"
watchChanges: true
printErrors: false
onLoaded: root.passwordPamConfigured = true
onLoadFailed: root.passwordPamConfigured = false
onFileChanged: reload()
}
Component.onCompleted: {
refreshBackground()
refreshFingerprintStatus()
}
IpcHandler {
target: "lock"
function lock(): string {
if (!root.passwordPamConfigured) return "missing-pam"
if (!root.locked && !root.beginLock()) return "failed"
return "ok"
}
function isLocked(): string {
return root.locked ? "true" : "false"
}
function status(): string {
return JSON.stringify({
locked: root.locked,
secure: sessionLock.secure,
passwordPam: root.passwordPamConfigured,
fingerprint: root.fingerprintConfigured,
authenticating: root.authenticating
})
}
function preview(): string {
root.refreshBackground()
root.refreshFingerprintStatus()
root.previewVisible = true
return "ok"
}
function hidePreview(): string {
root.previewVisible = false
return "ok"
}
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"schemaVersion": 1,
"id": "omarchy.lock",
"name": "Lock Screen",
"version": "1.0.0",
"author": "Omarchy",
"description": "Quickshell session lock with separate password and fingerprint PAM flows.",
"kinds": [
"service"
],
"keepLoaded": true,
"entryPoints": {
"service": "Service.qml"
}
}
-6
View File
@@ -1,6 +0,0 @@
$color = rgba(12, 11, 12, 1)
$inner_color = rgba(12, 11, 12, 0.66)
$outer_color = rgba(195, 139, 123, 1)
$font_color = rgba(226, 221, 220, 1)
$placeholder_color = rgba(255, 255, 255, 0.7)
$check_color = rgba(181, 151, 144, 1)
-6
View File
@@ -1,6 +0,0 @@
$color = rgba(16, 19, 21, 1)
$inner_color = rgba(16, 19, 21, 1)
$outer_color = rgba(121, 129, 134, 1)
$font_color = rgba(202, 204, 204, 1)
$placeholder_color = rgba(202, 204, 204, 0.7)
$check_color = rgba(52, 61, 65, 1)