breadgreet: skip the username step, fix clobbered typography
Some checks failed
CI / check (pull_request) Failing after 5s

Login UX
- Enumerate human accounts from /etc/passwd (UID_MIN..UID_MAX per
  /etc/login.defs, real login shell, minus nobody) and skip the
  username field: one account goes straight to the password prompt
  with the name shown, several get a picker. Zero found falls back to
  typing. New [user] config: `name` pins one account, `prompt = true`
  restores the type-it flow.
- The greeter opens the greetd conversation at startup in auto mode so
  it lands directly on the password prompt (AutoStart).
- reset_to_username -> reset_auth: auto mode re-opens the password
  prompt for the same account on error/Escape rather than showing a
  username field it never had.

Typography fix
- bind_window_auto re-broadcasts the shared component sheet (incl. its
  `* { font-size }` base rule) at USER-10, which outranks the
  APPLICATION-priority provider apply_app_css uses regardless of
  selector specificity -- so the hero clock and every other type
  override silently collapsed to base size. Ride breadgreet's sheet
  through bind_window_auto_with_app_css instead (USER-9), matching
  breadbar.

Polish
- Accent rule between the clock and card, drawn in via scaleX with a
  glow halo; card gradient + top highlight + deep shadow, bg-pop
  overshoot entrance, slow bg-breathe idle pulse; layered accent focus
  glow on the entry; gradient session/user pills. Entrance/idle motion
  is CSS @keyframes now (setup_entrance removed).
- Errors pinned to a legible red -- BOS's default @red slot is a warm
  ochre, unreadable as a warning.
- Drop the duplicate prompt text (placeholder + status line both said
  "Password:").

Preview harness
- scripts/preview.sh + scripts/mock-greetd.py run the real greeter in a
  nested Weston window against a stand-in greetd, so the whole flow
  (spinner, wrong-password shake, success) is drivable without a
  reboot. $BREADGREET_CONFIG overrides the config search path so a
  preview never touches /etc/greetd/breadgreet.toml.
This commit is contained in:
Breadway 2026-08-31 21:23:06 +08:00
parent 7ac3fdfc85
commit 8232f1b72b
7 changed files with 770 additions and 114 deletions

114
breadgreet/scripts/mock-greetd.py Executable file
View file

@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""A stand-in greetd for previewing breadgreet without a real session.
Speaks the greetd IPC wire format (`u32` native-endian length prefix + JSON
body, see the `greetd_ipc` crate) on a Unix socket. It never touches PAM and
never starts anything `start_session` just acknowledges and the greeter
exits, exactly as it would on a real login.
./mock-greetd.py /run/user/1000/breadgreet-preview.sock [password]
Default password is "bread"; any other answer gets the auth-error path so you
can see the shake + red status line.
"""
import json
import os
import socket
import struct
import sys
PASSWORD = sys.argv[2] if len(sys.argv) > 2 else "bread"
def read_frame(conn):
hdr = b""
while len(hdr) < 4:
chunk = conn.recv(4 - len(hdr))
if not chunk:
return None
hdr += chunk
(length,) = struct.unpack("=I", hdr)
body = b""
while len(body) < length:
chunk = conn.recv(length - len(body))
if not chunk:
return None
body += chunk
return json.loads(body)
def send(conn, obj):
body = json.dumps(obj).encode()
conn.sendall(struct.pack("=I", len(body)) + body)
def handle(conn):
while True:
req = read_frame(conn)
if req is None:
return
kind = req.get("type")
if kind == "create_session":
print(f" create_session username={req.get('username')!r}")
send(conn, {
"type": "auth_message",
"auth_message_type": "secret",
"auth_message": "Password: ",
})
elif kind == "post_auth_message_response":
if req.get("response") == PASSWORD:
print(" auth ok -> success")
send(conn, {"type": "success"})
else:
print(" auth bad -> auth_error")
send(conn, {
"type": "error",
"error_type": "auth_error",
"description": "Login incorrect",
})
elif kind == "start_session":
print(f" start_session cmd={req.get('cmd')}")
send(conn, {"type": "success"})
elif kind == "cancel_session":
print(" cancel_session")
send(conn, {"type": "success"})
else:
print(f" ?? {req}")
send(conn, {
"type": "error",
"error_type": "error",
"description": f"mock-greetd: unknown request {kind}",
})
def main():
sys.stdout.reconfigure(line_buffering=True)
if len(sys.argv) < 2:
sys.exit(f"usage: {sys.argv[0]} <socket-path> [password]")
path = sys.argv[1]
try:
os.unlink(path)
except FileNotFoundError:
pass
srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
srv.bind(path)
srv.listen(1)
print(f"mock-greetd listening on {path} (password: {PASSWORD!r})")
try:
while True:
conn, _ = srv.accept()
with conn:
handle(conn)
except KeyboardInterrupt:
pass
finally:
srv.close()
try:
os.unlink(path)
except FileNotFoundError:
pass
if __name__ == "__main__":
main()