From 7ea4e1ab0436ec604f0f1f400913b8fa306aea84 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Tue, 19 May 2026 17:08:14 +0200 Subject: [PATCH] Switch hyprlock to QS --- AGENTS.md | 9 - bin/omarchy-debug-idle | 55 ++++ bin/omarchy-font-set | 2 - bin/omarchy-launch-config-editor | 2 +- bin/omarchy-refresh-config | 6 +- bin/omarchy-refresh-hyprlock | 5 - bin/omarchy-remove-security-fingerprint | 12 +- bin/omarchy-restart-shell | 6 +- bin/omarchy-setup-lock | 62 +++++ bin/omarchy-setup-security-fingerprint | 18 +- bin/omarchy-style-corners | 3 +- bin/omarchy-style-corners-hyprlock | 35 --- bin/omarchy-system-lock | 11 +- config/hypr/hyprlock.conf | 45 ---- default/omarchy-skill/SKILL.md | 4 +- default/omarchy/omarchy-menu.jsonc | 2 - default/themed/hyprlock.conf.tpl | 5 - install/config/all.sh | 1 + install/config/increase-sudo-tries.sh | 2 +- install/config/lockscreen-pam.sh | 1 + install/config/toggles.sh | 1 - install/omarchy-base.packages | 1 - migrations/1759270604.sh | 3 - migrations/1762684663.sh | 12 - migrations/1762685081.sh | 12 - migrations/1764378788.sh | 5 - migrations/1770372978.sh | 5 - migrations/1779184948.sh | 9 + shell/plugins/README.md | 9 + shell/plugins/idle/Service.qml | 163 +++++++++--- shell/plugins/lock/LockView.qml | 152 +++++++++++ shell/plugins/lock/Service.qml | 318 ++++++++++++++++++++++++ shell/plugins/lock/manifest.json | 15 ++ themes/last-horizon/hyprlock.conf | 6 - themes/solitude/hyprlock.conf | 6 - 35 files changed, 784 insertions(+), 219 deletions(-) create mode 100755 bin/omarchy-debug-idle delete mode 100755 bin/omarchy-refresh-hyprlock create mode 100755 bin/omarchy-setup-lock delete mode 100755 bin/omarchy-style-corners-hyprlock delete mode 100644 config/hypr/hyprlock.conf delete mode 100644 default/themed/hyprlock.conf.tpl create mode 100644 install/config/lockscreen-pam.sh delete mode 100644 migrations/1759270604.sh delete mode 100644 migrations/1762684663.sh delete mode 100644 migrations/1762685081.sh delete mode 100644 migrations/1764378788.sh delete mode 100644 migrations/1770372978.sh create mode 100644 migrations/1779184948.sh create mode 100644 shell/plugins/lock/LockView.qml create mode 100644 shell/plugins/lock/Service.qml create mode 100644 shell/plugins/lock/manifest.json delete mode 100644 themes/last-horizon/hyprlock.conf delete mode 100644 themes/solitude/hyprlock.conf diff --git a/AGENTS.md b/AGENTS.md index 014ed3a5..587be065 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 -``` diff --git a/bin/omarchy-debug-idle b/bin/omarchy-debug-idle new file mode 100755 index 00000000..db97c30e --- /dev/null +++ b/bin/omarchy-debug-idle @@ -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 diff --git a/bin/omarchy-font-set b/bin/omarchy-font-set index 5935bcea..090d9774 100755 --- a/bin/omarchy-font-set +++ b/bin/omarchy-font-set @@ -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 \ diff --git a/bin/omarchy-launch-config-editor b/bin/omarchy-launch-config-editor index 8f4bdcf9..ffe100e9 100755 --- a/bin/omarchy-launch-config-editor +++ b/bin/omarchy-launch-config-editor @@ -2,7 +2,7 @@ # omarchy:summary=Open a config file in the user's editor and surface a toast # omarchy:args= -# omarchy:examples=omarchy launch config-editor ~/.config/hypr/hyprlock.conf +# omarchy:examples=omarchy launch config-editor ~/.config/hypr/hyprland.lua path="${1-}" diff --git a/bin/omarchy-refresh-config b/bin/omarchy-refresh-config index a7616100..7f49f298 100755 --- a/bin/omarchy-refresh-config +++ b/bin/omarchy-refresh-config @@ -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)" diff --git a/bin/omarchy-refresh-hyprlock b/bin/omarchy-refresh-hyprlock deleted file mode 100755 index 1a09c88f..00000000 --- a/bin/omarchy-refresh-hyprlock +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -# omarchy:summary=Overwrite the user config for hyprlock with the Omarchy default. - -omarchy-refresh-config hypr/hyprlock.conf diff --git a/bin/omarchy-remove-security-fingerprint b/bin/omarchy-remove-security-fingerprint index 1bbc7982..cdb54387 100755 --- a/bin/omarchy-remove-security-fingerprint +++ b/bin/omarchy-remove-security-fingerprint @@ -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 diff --git a/bin/omarchy-restart-shell b/bin/omarchy-restart-shell index a63a2a46..78ed69fc 100755 --- a/bin/omarchy-restart-shell +++ b/bin/omarchy-restart-shell @@ -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 & diff --git a/bin/omarchy-setup-lock b/bin/omarchy-setup-lock new file mode 100755 index 00000000..e8969682 --- /dev/null +++ b/bin/omarchy-setup-lock @@ -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." diff --git a/bin/omarchy-setup-security-fingerprint b/bin/omarchy-setup-security-fingerprint index f704348e..6c844ba3 100755 --- a/bin/omarchy-setup-security-fingerprint +++ b/bin/omarchy-setup-security-fingerprint @@ -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 = 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 diff --git a/bin/omarchy-style-corners b/bin/omarchy-style-corners index 76b6e7bd..39d0700c 100755 --- a/bin/omarchy-style-corners +++ b/bin/omarchy-style-corners @@ -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= # 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 diff --git a/bin/omarchy-style-corners-hyprlock b/bin/omarchy-style-corners-hyprlock deleted file mode 100755 index bc0a9f8b..00000000 --- a/bin/omarchy-style-corners-hyprlock +++ /dev/null @@ -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" </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 ) & diff --git a/config/hypr/hyprlock.conf b/config/hypr/hyprlock.conf deleted file mode 100644 index 10fffb33..00000000 --- a/config/hypr/hyprlock.conf +++ /dev/null @@ -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 = $FAIL ($ATTEMPTS) - - rounding = 0 - shadow_passes = 0 - fade_on_empty = false -} - -auth { - fingerprint:enabled = false -} - -source = ~/.local/state/omarchy/toggles/hyprlock.conf diff --git a/default/omarchy-skill/SKILL.md b/default/omarchy-skill/SKILL.md index 22c261e2..4a644a36 100644 --- a/default/omarchy-skill/SKILL.md +++ b/default/omarchy-skill/SKILL.md @@ -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 # 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 # Full reinstall of configs (nuclear option) diff --git a/default/omarchy/omarchy-menu.jsonc b/default/omarchy/omarchy-menu.jsonc index 905365aa..01abdfa1 100644 --- a/default/omarchy/omarchy-menu.jsonc +++ b/default/omarchy/omarchy-menu.jsonc @@ -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"}, diff --git a/default/themed/hyprlock.conf.tpl b/default/themed/hyprlock.conf.tpl deleted file mode 100644 index 1b4cac3c..00000000 --- a/default/themed/hyprlock.conf.tpl +++ /dev/null @@ -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) diff --git a/install/config/all.sh b/install/config/all.sh index 0c3b8398..41521144 100644 --- a/install/config/all.sh +++ b/install/config/all.sh @@ -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 diff --git a/install/config/increase-sudo-tries.sh b/install/config/increase-sudo-tries.sh index af5a61a6..3f812ff9 100644 --- a/install/config/increase-sudo-tries.sh +++ b/install/config/increase-sudo-tries.sh @@ -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 diff --git a/install/config/lockscreen-pam.sh b/install/config/lockscreen-pam.sh new file mode 100644 index 00000000..6056f2dc --- /dev/null +++ b/install/config/lockscreen-pam.sh @@ -0,0 +1 @@ +omarchy-setup-lock diff --git a/install/config/toggles.sh b/install/config/toggles.sh index ace601fa..0b7f168d 100644 --- a/install/config/toggles.sh +++ b/install/config/toggles.sh @@ -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 diff --git a/install/omarchy-base.packages b/install/omarchy-base.packages index 50306221..5a8f0a57 100644 --- a/install/omarchy-base.packages +++ b/install/omarchy-base.packages @@ -52,7 +52,6 @@ gvfs-smb hyprland hyprland-guiutils hyprland-preview-share-picker -hyprlock hyprpicker hyprsunset imagemagick diff --git a/migrations/1759270604.sh b/migrations/1759270604.sh deleted file mode 100644 index dce79803..00000000 --- a/migrations/1759270604.sh +++ /dev/null @@ -1,3 +0,0 @@ -echo "Update Hyprlock with better placeholder position and show all fail text" - -omarchy-refresh-hyprlock diff --git a/migrations/1762684663.sh b/migrations/1762684663.sh deleted file mode 100644 index 0d35aecb..00000000 --- a/migrations/1762684663.sh +++ /dev/null @@ -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 diff --git a/migrations/1762685081.sh b/migrations/1762685081.sh deleted file mode 100644 index 5f48b054..00000000 --- a/migrations/1762685081.sh +++ /dev/null @@ -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 = 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 diff --git a/migrations/1764378788.sh b/migrations/1764378788.sh deleted file mode 100644 index de9b5f13..00000000 --- a/migrations/1764378788.sh +++ /dev/null @@ -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 diff --git a/migrations/1770372978.sh b/migrations/1770372978.sh deleted file mode 100644 index 8ce213b6..00000000 --- a/migrations/1770372978.sh +++ /dev/null @@ -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 diff --git a/migrations/1779184948.sh b/migrations/1779184948.sh new file mode 100644 index 00000000..fcbee559 --- /dev/null +++ b/migrations/1779184948.sh @@ -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 diff --git a/shell/plugins/README.md b/shell/plugins/README.md index 26599d13..1282e5ec 100644 --- a/shell/plugins/README.md +++ b/shell/plugins/README.md @@ -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 diff --git a/shell/plugins/idle/Service.qml b/shell/plugins/idle/Service.qml index 52e36ed3..79c0cdd4 100644 --- a/shell/plugins/idle/Service.qml +++ b/shell/plugins/idle/Service.qml @@ -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) } diff --git a/shell/plugins/lock/LockView.qml b/shell/plugins/lock/LockView.qml new file mode 100644 index 00000000..085ada9b --- /dev/null +++ b/shell/plugins/lock/LockView.qml @@ -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 + } + } + } +} diff --git a/shell/plugins/lock/Service.qml b/shell/plugins/lock/Service.qml new file mode 100644 index 00000000..a0fc9b09 --- /dev/null +++ b/shell/plugins/lock/Service.qml @@ -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" + } + } +} diff --git a/shell/plugins/lock/manifest.json b/shell/plugins/lock/manifest.json new file mode 100644 index 00000000..87acd939 --- /dev/null +++ b/shell/plugins/lock/manifest.json @@ -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" + } +} diff --git a/themes/last-horizon/hyprlock.conf b/themes/last-horizon/hyprlock.conf deleted file mode 100644 index 87943568..00000000 --- a/themes/last-horizon/hyprlock.conf +++ /dev/null @@ -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) diff --git a/themes/solitude/hyprlock.conf b/themes/solitude/hyprlock.conf deleted file mode 100644 index 9303b47f..00000000 --- a/themes/solitude/hyprlock.conf +++ /dev/null @@ -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)