#!/usr/bin/env python3 """tanin-greet — TANINUX login greeter (GTK4 / PyGObject). Custom layout: a black top bar like the eww topbar (brand · clock · power), a wallpaper background, user + password centered as pills, and a small session selector tucked in the bottom-right corner. Talks to greetd over GREETD_SOCK. Run by greetd via cage: cage -s -- tanin-greet Preview without greetd: just run it — the UI renders; only login needs the socket (set GREETD_SOCK via a mock for a full dry-run). Env overrides (handy for previewing from the repo): TANIN_GREET_CSS path to the stylesheet (default /etc/greetd/tanin-greet.css) TANIN_GREET_BG wallpaper image path (default /usr/share/backgrounds/tanin/login.jpg) TANIN_GREET_SESSIONS wayland-sessions dir (default /usr/share/wayland-sessions) """ import gi, os, sys, json, socket, struct, shlex, glob, pwd gi.require_version("Gtk", "4.0") from gi.repository import Gtk, GLib, Gdk, Gio # noqa: E402 CSS_PATH = os.environ.get("TANIN_GREET_CSS", "/etc/greetd/tanin-greet.css") WALLPAPER = os.environ.get("TANIN_GREET_BG", "/usr/share/backgrounds/tanin/login.jpg") SESSION_DIR = os.environ.get("TANIN_GREET_SESSIONS", "/usr/share/wayland-sessions") BRAND = os.environ.get("TANIN_GREET_BRAND", "TANINUX") # --------------------------------------------------------------------------- # # greetd IPC client — native-endian u32 length prefix + JSON payload. # --------------------------------------------------------------------------- # class Greetd: def __init__(self): path = os.environ.get("GREETD_SOCK") self.sock = None if path: try: self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.sock.connect(path) except OSError: # No reachable greetd (e.g. preview without a daemon). Render the # UI anyway; login() will report the missing socket on submit. self.sock = None def _recvn(self, n): buf = b"" while len(buf) < n: chunk = self.sock.recv(n - len(buf)) if not chunk: raise RuntimeError("greetd closed the connection") buf += chunk return buf def _send(self, obj): data = json.dumps(obj).encode() self.sock.sendall(struct.pack("=I", len(data)) + data) def _recv(self): n = struct.unpack("=I", self._recvn(4))[0] return json.loads(self._recvn(n)) def login(self, username, password, cmd_argv): """Authenticate and start the session. Raises on failure.""" if self.sock is None: raise RuntimeError("GREETD_SOCK not set (preview mode — cannot log in)") self._send({"type": "create_session", "username": username}) while True: r = self._recv() t = r.get("type") if t == "auth_message": amt = r.get("auth_message_type") if amt in ("secret", "visible"): self._send({"type": "post_auth_message_response", "response": password}) else: # info / error — acknowledge with no response self._send({"type": "post_auth_message_response"}) elif t == "success": break elif t == "error": self._send({"type": "cancel_session"}) raise RuntimeError(r.get("description", "authentication failed")) else: raise RuntimeError(f"unexpected greetd reply: {r}") # authenticated → launch the chosen session self._send({"type": "start_session", "cmd": cmd_argv, "env": []}) r = self._recv() if r.get("type") != "success": self._send({"type": "cancel_session"}) raise RuntimeError(r.get("description", "failed to start session")) # --------------------------------------------------------------------------- # # System enumeration # --------------------------------------------------------------------------- # def list_users(): users = [] for p in pwd.getpwall(): if 1000 <= p.pw_uid < 65534 and p.pw_shell not in ("/usr/bin/nologin", "/sbin/nologin", "/bin/false"): users.append(p.pw_name) return users or [os.environ.get("USER", "user")] def list_sessions(): sessions = [] # (name, exec_argv) for f in sorted(glob.glob(os.path.join(SESSION_DIR, "*.desktop"))): name = exec_ = None try: for line in open(f, encoding="utf-8"): if line.startswith("Name=") and name is None: name = line[5:].strip() elif line.startswith("Exec=") and exec_ is None: exec_ = line[5:].strip() except OSError: continue if name and exec_: sessions.append((name, shlex.split(exec_))) return sessions or [("Niri", ["niri-session"])] # --------------------------------------------------------------------------- # # UI # --------------------------------------------------------------------------- # class Greeter(Gtk.Application): def __init__(self): super().__init__(application_id="ch.gabrielevarano.TaninGreet") self.greetd = Greetd() self.users = list_users() self.sessions = list_sessions() def do_activate(self): self._load_css() Gtk.Settings.get_default().set_property("gtk-application-prefer-dark-theme", True) win = Gtk.ApplicationWindow(application=self) win.set_decorated(False) win.fullscreen() win.add_css_class("greeter") overlay = Gtk.Overlay() win.set_child(overlay) # background image (falls back to the scrim colour if missing) if os.path.exists(WALLPAPER): bg = Gtk.Picture.new_for_filename(WALLPAPER) bg.set_content_fit(Gtk.ContentFit.COVER) overlay.set_child(bg) else: overlay.set_child(Gtk.Box()) # dark scrim so text stays readable over any wallpaper scrim = Gtk.Box() scrim.add_css_class("scrim") scrim.set_hexpand(True) scrim.set_vexpand(True) overlay.add_overlay(scrim) # main column: top bar + centered login col = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) col.append(self._topbar()) center = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) center.set_vexpand(True) center.set_valign(Gtk.Align.CENTER) center.set_halign(Gtk.Align.CENTER) center.append(self._login_card()) col.append(center) col.append(self._footer()) # TANINUX wordmark, bottom center overlay.add_overlay(col) win.present() self.password.grab_focus() # debug: auto-open the session dropdown so previews can screenshot the # popover without a click (TANIN_GREET_POPUP=1). No effect in production. if os.environ.get("TANIN_GREET_POPUP"): def _open(): btn = self.session_dd.get_first_child() try: btn.set_active(True) except Exception: # noqa: BLE001 pass return False GLib.timeout_add(900, _open) # ---- pieces ----------------------------------------------------------- # def _topbar(self): bar = Gtk.CenterBox() bar.add_css_class("topbar") # top-left corner: session dropdown bar.set_start_widget(self._session_dd()) # center: clock — time · weekday, date self.clock = Gtk.Label() self.clock.add_css_class("clock") bar.set_center_widget(self.clock) self._tick() GLib.timeout_add_seconds(1, self._tick) # top-right corner: power bar.set_end_widget(self._power_button()) return bar def _session_dd(self): self.session_dd = Gtk.DropDown.new_from_strings([s[0] for s in self.sessions]) self.session_dd.add_css_class("session-dd") for i, (name, _) in enumerate(self.sessions): if name.lower().startswith("niri"): self.session_dd.set_selected(i) break return self.session_dd def _footer(self): box = Gtk.Box() box.set_halign(Gtk.Align.CENTER) box.set_valign(Gtk.Align.END) box.set_margin_bottom(18) lbl = Gtk.Label(label=BRAND) lbl.add_css_class("brand") box.append(lbl) return box def _power_button(self): btn = Gtk.MenuButton() btn.add_css_class("power") btn.set_icon_name("system-shutdown-symbolic") pop = Gtk.Popover() pop.add_css_class("menu") box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) for label, argv in (("Reboot", ["systemctl", "reboot"]), ("Power Off", ["systemctl", "poweroff"])): b = Gtk.Button(label=label) b.add_css_class("menuitem") b.connect("clicked", lambda _w, a=argv: self._run(a)) box.append(b) pop.set_child(box) btn.set_popover(pop) return btn def _login_card(self): card = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=14) card.add_css_class("logincard") # NOT "card" — libadwaita's .card adds a bg+shadow card.set_halign(Gtk.Align.CENTER) avatar = Gtk.Image.new_from_icon_name("avatar-default-symbolic") avatar.set_pixel_size(64) avatar.add_css_class("avatar") avatar.set_halign(Gtk.Align.CENTER) card.append(avatar) # user row — just the NAME as text, with a small dropdown chevron on the # right edge (aligned with where the password pill ends). Not a pill. name_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) name_row.add_css_class("namerow") name_row.set_halign(Gtk.Align.CENTER) self.name_label = Gtk.Label(label=self.users[0]) self.name_label.add_css_class("username") self.name_label.set_xalign(0.5) # centered self.name_label.set_hexpand(True) name_row.append(self.name_label) self.user_btn = Gtk.MenuButton() self.user_btn.add_css_class("userswitch") self.user_btn.set_icon_name("pan-down-symbolic") self.user_btn.set_visible(len(self.users) > 1) # no point if there's one user upop = Gtk.Popover() upop.add_css_class("menu") ubox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2) for u in self.users: b = Gtk.Button(label=u) b.add_css_class("menuitem") b.connect("clicked", lambda _w, name=u: (self.name_label.set_text(name), self.user_btn.popdown())) ubox.append(b) upop.set_child(ubox) self.user_btn.set_popover(upop) name_row.append(self.user_btn) card.append(name_row) # password pill self.password = Gtk.PasswordEntry() self.password.set_show_peek_icon(True) self.password.set_property("placeholder-text", "Password") self.password.add_css_class("pill") self.password.connect("activate", self._on_submit) card.append(self.password) self.error = Gtk.Label() self.error.add_css_class("error") self.error.set_visible(False) card.append(self.error) return card # ---- behaviour -------------------------------------------------------- # def _tick(self): now = GLib.DateTime.new_now_local() self.clock.set_text(now.format("%H:%M · %A, %d %B %Y")) return True def _on_submit(self, _entry): user = self.name_label.get_text() pw = self.password.get_text() cmd = self.sessions[self.session_dd.get_selected()][1] try: self.greetd.login(user, pw, cmd) self.quit() # greetd takes over from here except Exception as e: # noqa: BLE001 self.password.set_text("") self.error.set_text(str(e)) self.error.set_visible(True) def _run(self, argv): try: Gio.Subprocess.new(argv, Gio.SubprocessFlags.NONE) except GLib.Error as e: self.error.set_text(str(e)) self.error.set_visible(True) def _load_css(self): if not os.path.exists(CSS_PATH): return provider = Gtk.CssProvider() provider.load_from_path(CSS_PATH) Gtk.StyleContext.add_provider_for_display( Gdk.Display.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_USER) if __name__ == "__main__": sys.exit(Greeter().run(None))