Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 87 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,36 @@ follows macOS Sonoma.

![The greeter and the locker, before and after the first key](docs/comparison.png)

## Install
Arch Linux:

```sh
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
```

That builds both programs, puts them in `/usr/local/bin`, and writes five
things that keep the two screens the same:

| File | Content |
| --- | --- |
| `/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.lua` | The monitor modes of your desktop |
| `/var/lib/AccountsService/icons/<user>` | Your avatar |

The script calls `sudo` only for the steps outside the repository, and it
keeps a copy of every file it replaces.

| Option | What it adds |
| --- | --- |
| `--greeter` | The greetd files in `/etc/greetd`, and the monitor modes |
| `--enable-greetd` | greetd at boot. This turns on `--greeter` |
| `--wallpaper PATH`, `--avatar PATH`, `--font NAME` | Your own choices |
| `--prefix DIR`, `--destdir DIR` | Another place, or a package build |
| `--dry-run`, `--uninstall` | The steps only, or remove everything |
The greeter adds the power buttons, the user picker, and the session list.
Nothing else differs.

Expand All @@ -17,6 +47,23 @@ Nothing else differs.
Install the dependencies, then run the script:

```sh
sudo systemctl disable sddm.service # or your display manager
sudo systemctl enable greetd.service
```

Keep the old display manager installed. If the greeter fails at boot, press
Ctrl+Alt+F2 for a text login and turn it back on.

## Make it your screen locker

Bind a key, and point your idle daemon at the same command:

```lua
hl.bind("SUPER + L", hl.dsp.exec_cmd("pgrep -x psldm-lock || psldm-lock"))
```

`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.
sudo pacman -S --needed rust clang gtk4 gtk4-layer-shell pam accountsservice greetd
git clone https://github.com/DNAPrototypeX/PSLDM.git
cd PSLDM
Expand All @@ -40,6 +87,11 @@ remove everything again.
`misc:allow_session_lock_restore`, which lets a new locker take the lock back
after a crash.

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 also starts
the password, so no character is lost.

## Log in with the greeter

1. Test the greeter: `psldm-greet --preview ~/Pictures/wallpaper.png`.
Expand Down Expand Up @@ -88,9 +140,41 @@ GitHub Actions runs the same three commands in an Arch Linux container. See

This command draws the picture at the top of this page:

```sh
cargo run -p psldm-ui --example comparison -- docs/comparison.png
```
The test needs a Wayland or an X11 display, and it opens two windows for a
moment. Without a display it reports the reason and stops. Set
`PSLDM_TEST_DUMP` to a directory to save the drawings as PPM files.

## Requirements

| Package | Needed for | Without it |
| --- | --- | --- |
| `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` 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
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.lua` configures that compositor. Another
compositor needs its own file.
- `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
everywhere.

## License

Expand Down
8 changes: 4 additions & 4 deletions crates/psldm-ui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand All @@ -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
});
}
Expand Down
42 changes: 32 additions & 10 deletions crates/psldm-ui/src/pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use std::cell::{Cell, RefCell};
use std::path::PathBuf;
use std::rc::Rc;

use gtk::gdk;
use gtk::glib;
use gtk::prelude::*;

Expand Down Expand Up @@ -226,28 +227,33 @@ 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();
}

/// 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);

Expand Down Expand Up @@ -427,6 +433,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<char> {
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<gtk::Widget>, class: &str, present: bool) {
if present {
Expand Down
3 changes: 2 additions & 1 deletion crates/psldm-ui/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
33 changes: 19 additions & 14 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)} }})")
'
}

Expand All @@ -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
Expand All @@ -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" \
Expand All @@ -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 ""
Expand All @@ -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"
Expand Down
9 changes: 6 additions & 3 deletions packaging/greetd/greeter-session
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 0 additions & 29 deletions packaging/greetd/hyprland.conf

This file was deleted.

Loading
Loading