From 8afec2a52c7c61e93f7b92081b2fde459c03daca Mon Sep 17 00:00:00 2001 From: Paul Moore Date: Fri, 4 Sep 2026 14:42:48 -0400 Subject: [PATCH 1/2] Move the Hyprland files to the Lua format Hyprland 0.56 reads a Lua configuration, and it plans to drop the older `.conf` format. Both files that PSLDM ships now use Lua, and install.sh writes the monitor modes as Lua. - `packaging/greetd/hyprland.conf` becomes `hyprland.lua`. It reads the monitor file with `pcall(dofile, ...)`, so a missing file is not an error. The old `source =` line failed in that case. - `packaging/hypr/psldm-lock.conf` becomes `psldm-lock.lua`. - `/etc/psldm/monitors.conf` becomes `/etc/psldm/monitors.lua`, and it holds `hl.monitor` calls. The greeter session also starts the compositor with `start-hyprland`, the watchdog that Hyprland ships. It restarts the compositor after an unclean exit, so a crash brings the greeter back instead of a text console. The greeter still ends the compositor with `hyprctl dispatch exit`, which is a clean exit, so greetd gets control back. `Hyprland --verify-config` reports `config ok` for both files, and for a `monitors.lua` written from a running desktop. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JoZyLsqdiAe7AAqdxvdvti --- README.md | 20 +++++++++------- install.sh | 33 +++++++++++++++----------- packaging/greetd/greeter-session | 9 ++++--- packaging/greetd/hyprland.conf | 29 ----------------------- packaging/greetd/hyprland.lua | 40 ++++++++++++++++++++++++++++++++ packaging/hypr/psldm-lock.conf | 20 ---------------- packaging/hypr/psldm-lock.lua | 22 ++++++++++++++++++ 7 files changed, 99 insertions(+), 74 deletions(-) delete mode 100644 packaging/greetd/hyprland.conf create mode 100644 packaging/greetd/hyprland.lua delete mode 100644 packaging/hypr/psldm-lock.conf create mode 100644 packaging/hypr/psldm-lock.lua diff --git a/README.md b/README.md index b917a81..5751cc9 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ differs. Arch Linux: ```sh -sudo pacman -S rust gtk4 gtk4-layer-shell greetd accountsservice hyprland +sudo pacman -S rust clang gtk4 gtk4-layer-shell greetd accountsservice hyprland git clone https://github.com/DNAPrototypeX/PSLDM && cd PSLDM ./install.sh --greeter --wallpaper ~/path/to/wallpaper.jpg ``` @@ -26,7 +26,7 @@ things that keep the two screens the same: | `/etc/pam.d/psldm` | The PAM stack for the locker | | `/etc/psldm/wallpaper` | Your image, readable by the greeter user | | `/etc/psldm/font` | The font family of your desktop | -| `/etc/psldm/monitors.conf` | The monitor modes of your desktop | +| `/etc/psldm/monitors.lua` | The monitor modes of your desktop | | `/var/lib/AccountsService/icons/` | Your avatar | The script calls `sudo` only for the steps outside the repository, and it @@ -77,11 +77,11 @@ Ctrl+Alt+F2 for a text login and turn it back on. Bind a key, and point your idle daemon at the same command: -``` -bind = SUPER, L, exec, pgrep -x psldm-lock || psldm-lock +```lua +hl.bind("SUPER + L", hl.dsp.exec_cmd("pgrep -x psldm-lock || psldm-lock")) ``` -`packaging/hypr/psldm-lock.conf` holds that line for Hyprland. An idle daemon +`packaging/hypr/psldm-lock.lua` holds that line for Hyprland. An idle daemon needs the full path, because it often runs with a short PATH. ## The screen @@ -172,12 +172,16 @@ moment. Without a display it reports the reason and stops. Set | `gtk4`, `gtk4-layer-shell` 1.1 or later | Both programs. The session-lock API is part of gtk4-layer-shell | Nothing runs | | `greetd` | The greeter only | The locker still works | | `accountsservice` | The user list and the avatars | The greeter shows the user who runs it | -| A compositor for the greeter | `hyprland`, `sway`, or `cage` | The greeter has no screen | +| A compositor for the greeter | `hyprland` 0.56 or later, `sway`, or `cage` | The greeter has no screen | | `python3` | `install.sh`, to copy the monitor modes | The greeter uses the preferred mode | +| `clang` | The build only. The `pam-sys` crate runs `bindgen`, which loads `libclang` | The build stops at `clang-sys` | The compositor of your session must support `ext-session-lock-v1`. Hyprland, Sway, KDE, and River do. +The two Hyprland files use the Lua format, which Hyprland 0.56 reads. Hyprland +still reads the older `.conf` format, and it plans to drop it. + ## Other desktops PSLDM has no code for one distribution or one desktop. Three files hold every @@ -185,9 +189,9 @@ choice, and each one is easy to change: - `packaging/greetd/greeter-session` names the compositor for the greeter. Change one line for `cage` or `sway`. -- `packaging/greetd/hyprland.conf` configures that compositor. Another +- `packaging/greetd/hyprland.lua` configures that compositor. Another compositor needs its own file. -- `install.sh --greeter` writes `/etc/psldm/monitors.conf` with `hyprctl`. +- `install.sh --greeter` writes `/etc/psldm/monitors.lua` with `hyprctl`. Another compositor keeps the preferred mode of each monitor. The wallpaper, the font, the avatar, and the session memory work the same diff --git a/install.sh b/install.sh index b80c5cb..097b531 100755 --- a/install.sh +++ b/install.sh @@ -280,15 +280,15 @@ install_monitors() { return 0 fi - say "Copying the monitor settings to /etc/psldm/monitors.conf" + say "Copying the monitor settings to /etc/psldm/monitors.lua" if [[ "$DRY_RUN" -eq 1 ]]; then sed 's/^/ /' "$temporary" fi - run install -Dm644 "$temporary" "$(target /etc/psldm/monitors.conf)" + run install -Dm644 "$temporary" "$(target /etc/psldm/monitors.lua)" rm -f "$temporary" } -# Turn the JSON of hyprctl into Hyprland monitor lines. +# Turn the JSON of hyprctl into Hyprland monitor calls, in the Lua format. monitor_lines() { python3 -c ' import json, sys @@ -297,14 +297,19 @@ monitors = json.load(sys.stdin) if not monitors: raise SystemExit(1) -print("# Written by install.sh from the running desktop. Do not edit.") +print("-- Written by install.sh from the running desktop. Do not edit.") for monitor in monitors: mode = f"{monitor["width"]}x{monitor["height"]}@{monitor["refreshRate"]:.2f}" position = f"{monitor["x"]}x{monitor["y"]}" - line = f"monitor = {monitor["name"]}, {mode}, {position}, {monitor["scale"]}" + fields = [ + f"output = {json.dumps(monitor["name"])}", + f"mode = \"{mode}\"", + f"position = \"{position}\"", + f"scale = {monitor["scale"]}", + ] if monitor.get("transform"): - line += f", transform, {monitor["transform"]}" - print(line) + fields.append(f"transform = {monitor["transform"]}") + print(f"hl.monitor({{ {", ".join(fields)} }})") ' } @@ -325,7 +330,7 @@ install_greeter() { say "Installing the greetd files in /etc/greetd" back_up "$(target /etc/greetd/config.toml)" - back_up "$(target /etc/greetd/hyprland.conf)" + back_up "$(target /etc/greetd/hyprland.lua)" # Every file must name the programs where this script put them. local temporary @@ -336,8 +341,8 @@ install_greeter() { run install -Dm644 "$temporary" "$(target /etc/greetd/config.toml)" sed "s|/usr/bin/psldm-greet|$PREFIX/bin/psldm-greet|" \ - "$REPO/packaging/greetd/hyprland.conf" > "$temporary" - run install -Dm644 "$temporary" "$(target /etc/greetd/hyprland.conf)" + "$REPO/packaging/greetd/hyprland.lua" > "$temporary" + run install -Dm644 "$temporary" "$(target /etc/greetd/hyprland.lua)" rm -f "$temporary" run install -Dm755 "$REPO/packaging/greetd/greeter-session" \ @@ -361,8 +366,8 @@ report() { say "Test the locker now:" say " $PREFIX/bin/psldm-lock --preview" say "" - say "Add this line to ~/.config/hypr/hyprland.conf to lock with SUPER + L:" - say " bind = SUPER, L, exec, pgrep -x psldm-lock || $PREFIX/bin/psldm-lock" + say "Add this line to ~/.config/hypr/hyprland.lua to lock with SUPER + L:" + say " hl.bind(\"SUPER + L\", hl.dsp.exec_cmd(\"pgrep -x psldm-lock || $PREFIX/bin/psldm-lock\"))" if [[ "$WITH_GREETER" -eq 1 && "$ENABLE_GREETD" -eq 0 ]]; then say "" @@ -384,11 +389,11 @@ uninstall() { "$(target "$PREFIX/bin/psldm-greeter-session")" \ "$(target /etc/pam.d/psldm)" \ "$(target /etc/psldm/wallpaper)" \ - "$(target /etc/psldm/monitors.conf)" \ + "$(target /etc/psldm/monitors.lua)" \ "$(target /etc/psldm/font)" \ "$(target /var/lib/psldm/state.toml)" \ "$(target "/var/lib/AccountsService/icons/$(current_user)")" \ - "$(target /etc/greetd/hyprland.conf)" \ + "$(target /etc/greetd/hyprland.lua)" \ "$(target /etc/greetd/config.toml)"; do [[ -e "$path" ]] || continue say " removing $path" diff --git a/packaging/greetd/greeter-session b/packaging/greetd/greeter-session index b5d27ee..846296a 100755 --- a/packaging/greetd/greeter-session +++ b/packaging/greetd/greeter-session @@ -8,11 +8,14 @@ # Install it at /usr/bin/psldm-greeter-session, or wherever install.sh puts # the programs. /etc/greetd/config.toml names this file. -# The compositor that holds the greeter. Another compositor needs another -# command here, such as: +# The compositor that holds the greeter. start-hyprland is the watchdog that +# Hyprland ships. It restarts the compositor after a crash, so the greeter +# comes back instead of leaving a text console. +# +# Another compositor needs another command here, such as: # cage -s -- /usr/local/bin/psldm-greet # sway --config /etc/greetd/sway.conf -compositor="Hyprland --config /etc/greetd/hyprland.conf" +compositor="start-hyprland -- --config /etc/greetd/hyprland.lua" if ! command -v "${compositor%% *}" >/dev/null; then echo "psldm-greeter-session: ${compositor%% *} is not installed" >&2 diff --git a/packaging/greetd/hyprland.conf b/packaging/greetd/hyprland.conf deleted file mode 100644 index cd2239f..0000000 --- a/packaging/greetd/hyprland.conf +++ /dev/null @@ -1,29 +0,0 @@ -# Hyprland configuration for the PSLDM greeter. -# Install it at /etc/greetd/hyprland.conf. -# -# The compositor exists only to hold the greeter. It stops as soon as the -# greeter stops, and greetd then starts the user session. - -# The fallback, for a monitor that the next file does not name. -monitor = , preferred, auto, 1 - -# The modes of the desktop session. install.sh writes this file, so that the -# greeter and the session show the pane at the same size. -source = /etc/psldm/monitors.conf - -misc { - disable_hyprland_logo = true - disable_splash_rendering = true - force_default_wallpaper = 0 -} - -animations { - enabled = false -} - -# Set the same keyboard layout as the session, or the password fails. -input { - kb_layout = us -} - -exec-once = sh -c '/usr/bin/psldm-greet; hyprctl dispatch exit' diff --git a/packaging/greetd/hyprland.lua b/packaging/greetd/hyprland.lua new file mode 100644 index 0000000..e41d0e9 --- /dev/null +++ b/packaging/greetd/hyprland.lua @@ -0,0 +1,40 @@ +-- Hyprland configuration for the PSLDM greeter. +-- Install it at /etc/greetd/hyprland.lua. +-- +-- The compositor exists only to hold the greeter. It stops as soon as the +-- greeter stops, and greetd then starts the user session. + +-- The fallback, for a monitor that the next file does not name. +hl.monitor({ + output = "", + mode = "preferred", + position = "auto", + scale = 1, +}) + +-- The modes of the desktop session. install.sh writes this file, so that the +-- greeter and the session show the pane at the same size. The file is absent +-- until install.sh --greeter runs on a desktop that has hyprctl, so read it +-- with pcall. +pcall(dofile, "/etc/psldm/monitors.lua") + +hl.config({ + misc = { + disable_hyprland_logo = true, + disable_splash_rendering = true, + force_default_wallpaper = 0, + }, + + animations = { + enabled = false, + }, + + -- Set the same keyboard layout as the session, or the password fails. + input = { + kb_layout = "us", + }, +}) + +hl.on("hyprland.start", function() + hl.exec_cmd([[sh -c '/usr/bin/psldm-greet; hyprctl dispatch exit']]) +end) diff --git a/packaging/hypr/psldm-lock.conf b/packaging/hypr/psldm-lock.conf deleted file mode 100644 index aefce13..0000000 --- a/packaging/hypr/psldm-lock.conf +++ /dev/null @@ -1,20 +0,0 @@ -# Hyprland settings for the PSLDM screen locker. -# Add this line to ~/.config/hypr/hyprland.conf: -# source = /path/to/psldm-lock.conf - -# Lock the session with SUPER + L. -bind = SUPER, L, exec, pgrep -x psldm-lock || psldm-lock - -# hypridle can call the same command. Use `lock_cmd` in ~/.config/hypr/ -# hypridle.conf, and keep the pgrep guard, so that one lock surface runs at a -# time. - -# Let a new locker take over a lock whose program died. -# -# Without this, Hyprland keeps the screen locked and shows its own message, -# and no program can accept your password. You then need a text console to -# recover the session. With it, running psldm-lock again brings the password -# field back. -misc { - allow_session_lock_restore = true -} diff --git a/packaging/hypr/psldm-lock.lua b/packaging/hypr/psldm-lock.lua new file mode 100644 index 0000000..7e88d9b --- /dev/null +++ b/packaging/hypr/psldm-lock.lua @@ -0,0 +1,22 @@ +-- Hyprland settings for the PSLDM screen locker. +-- Add this line to ~/.config/hypr/hyprland.lua: +-- dofile("/path/to/psldm-lock.lua") + +-- Lock the session with SUPER + L. +hl.bind("SUPER + L", hl.dsp.exec_cmd("pgrep -x psldm-lock || psldm-lock")) + +-- hypridle can call the same command. Use `lock_cmd` in ~/.config/hypr/ +-- hypridle.conf, and keep the pgrep guard, so that one lock surface runs at a +-- time. + +-- Let a new locker take over a lock whose program died. +-- +-- Without this, Hyprland keeps the screen locked and shows its own message, +-- and no program can accept your password. You then need a text console to +-- recover the session. With it, running psldm-lock again brings the password +-- field back. +hl.config({ + misc = { + allow_session_lock_restore = true, + }, +}) From 89ab2d86973bf296e77f04fb2719d28c6820477f Mon Sep 17 00:00:00 2001 From: Paul Moore Date: Fri, 4 Sep 2026 14:42:57 -0400 Subject: [PATCH 2/2] Start the password with the key that wakes the screen The screen showed the clock only, and the first key woke the pane. The pane swallowed that key, so the user lost the first character of the password. The pane now puts the character in the field. The field is still sliding in and does not hold the keyboard yet, so the key controller writes the character instead of letting the event through. A new function, `typed_character`, drops a key that types nothing: - A key held with Control, Alt, or Super runs a command somewhere else. - A control character such as Escape or Tab is not part of a password. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JoZyLsqdiAe7AAqdxvdvti --- README.md | 4 ++-- crates/psldm-ui/src/app.rs | 8 +++---- crates/psldm-ui/src/pane.rs | 42 +++++++++++++++++++++++++++--------- crates/psldm-ui/src/state.rs | 3 ++- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 5751cc9..b3fd9cc 100644 --- a/README.md +++ b/README.md @@ -95,8 +95,8 @@ The layout follows macOS Sonoma. The screen has two phases. Before the first key it shows the picker only. The first key slides the password field in under the name over 260 -milliseconds, and the picker rises as the field grows. That key does not -reach the field. +milliseconds, and the picker rises as the field grows. That key also starts +the password, so no character is lost. The screen returns to the first phase after 30 seconds without a key, but only while the field is empty. A row of other users appears in the middle of diff --git a/crates/psldm-ui/src/app.rs b/crates/psldm-ui/src/app.rs index 08d378c..3f543d4 100644 --- a/crates/psldm-ui/src/app.rs +++ b/crates/psldm-ui/src/app.rs @@ -144,8 +144,8 @@ pub fn run(setup: AppSetup, backend: AuthHandle) -> glib::ExitCode { }); } - // The field keeps the focus while the screen shows the clock only, so - // the first key of the password lands in it. + // The field holds the keyboard from the start, so every key of the + // password lands in it. surfaces.focus_entry(); let last_input = Rc::new(Cell::new(Instant::now())); @@ -160,8 +160,8 @@ pub fn run(setup: AppSetup, backend: AuthHandle) -> glib::ExitCode { if woke { wake_surfaces.render(&wake_state.borrow()); } - // Swallow the key that wakes the screen. macOS does not put - // that first key in the password. + // Report the wake, so that the pane puts the key that woke + // the screen at the start of the password. woke }); } diff --git a/crates/psldm-ui/src/pane.rs b/crates/psldm-ui/src/pane.rs index b25956f..d93f8a0 100644 --- a/crates/psldm-ui/src/pane.rs +++ b/crates/psldm-ui/src/pane.rs @@ -11,6 +11,7 @@ use std::cell::RefCell; use std::path::PathBuf; use std::rc::Rc; +use gtk::gdk; use gtk::glib; use gtk::prelude::*; @@ -216,9 +217,6 @@ impl LoginPane { } /// Give the keyboard to the field. - /// - /// The field keeps the focus while the screen shows the clock only, so - /// the first key of the password lands in it. pub fn focus_entry(&self) { self.entry.grab_focus(); } @@ -226,18 +224,26 @@ impl LoginPane { /// Call `handler` on every key and on every click. /// /// The handler returns `true` when the screen was showing the clock only. - /// That first key wakes the screen, and the field does not receive it. - /// Every later key reaches the field. + /// That first key wakes the screen, and it also starts the password. The + /// field is still sliding in and does not hold the keyboard yet, so this + /// puts the character in the field. Every later key reaches the field on + /// its own. pub fn connect_activity(&self, handler: impl Fn() -> bool + Clone + 'static) { let keys = gtk::EventControllerKey::new(); keys.set_propagation_phase(gtk::PropagationPhase::Capture); let key_handler = handler.clone(); - keys.connect_key_pressed(move |_, _, _, _| { - if key_handler() { - glib::Propagation::Stop - } else { - glib::Propagation::Proceed + let entry = self.entry.clone(); + keys.connect_key_pressed(move |_, key, _, modifiers| { + if !key_handler() { + return glib::Propagation::Proceed; + } + entry.grab_focus(); + if let Some(character) = typed_character(key, modifiers) { + let mut position = entry.text_length() as i32; + entry.insert_text(&character.to_string(), &mut position); + entry.set_position(-1); } + glib::Propagation::Stop }); self.root.add_controller(keys); @@ -421,6 +427,22 @@ impl LoginPane { } /// Build the clock and the date, and update them every second. +/// The character that a key press types, or `None` for a key that types +/// nothing. +/// +/// A key held with Control, Alt, or Super runs a command somewhere else, so +/// it must not start the password. A control character such as Escape or Tab +/// must not either. +fn typed_character(key: gdk::Key, modifiers: gdk::ModifierType) -> Option { + let commands = gdk::ModifierType::CONTROL_MASK + | gdk::ModifierType::ALT_MASK + | gdk::ModifierType::SUPER_MASK; + if modifiers.intersects(commands) { + return None; + } + key.to_unicode().filter(|character| !character.is_control()) +} + /// Add or remove one class. fn set_css_class(widget: &impl IsA, class: &str, present: bool) { if present { diff --git a/crates/psldm-ui/src/state.rs b/crates/psldm-ui/src/state.rs index 04978eb..79d1d72 100644 --- a/crates/psldm-ui/src/state.rs +++ b/crates/psldm-ui/src/state.rs @@ -278,7 +278,8 @@ mod tests { fn every_return_to_the_clock_needs_a_new_first_key() { let mut state = LoginState::new(Mode::Lock, "paul"); - // The caller swallows the key whenever `wake` reports a change. + // The caller starts the password with the key whenever `wake` + // reports a change. for _ in 0..3 { assert!(state.wake()); assert!(!state.wake());