├── .gitignore
├── img
├── X.png
├── wayland.png
├── X.svg
└── wayland.svg
├── screenshot.png
├── lightdm-elephant-greeter-x11.desktop
├── lightdm-elephant-greeter.desktop
├── elephant-greeter.conf.base
├── Makefile
├── LICENSE
├── README.md
├── elephant-greeter.ui
└── elephant-greeter.py
/.gitignore:
--------------------------------------------------------------------------------
1 | elephant-greeter.conf
2 |
--------------------------------------------------------------------------------
/img/X.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozlima/lightdm-elephant-greeter/main/img/X.png
--------------------------------------------------------------------------------
/img/wayland.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozlima/lightdm-elephant-greeter/main/img/wayland.png
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozlima/lightdm-elephant-greeter/main/screenshot.png
--------------------------------------------------------------------------------
/lightdm-elephant-greeter-x11.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=The Elephant LightDM Greeter (X11)
3 | Comment=Small and simple LightDM Greeter written in Python, using X11
4 | Exec=elephant-greeter.py
5 | Type=Application
6 | X-LightDM-Session-Type=x
7 |
8 |
--------------------------------------------------------------------------------
/lightdm-elephant-greeter.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=The Elephant LightDM Greeter (Wayland)
3 | Comment=Small and simple LightDM Greeter written in Python, using Cage (Wayland)
4 | Exec=cage -m last -s -d elephant-greeter.py
5 | Type=Application
6 | X-LightDM-Session-Type=wayland
7 |
8 |
--------------------------------------------------------------------------------
/elephant-greeter.conf.base:
--------------------------------------------------------------------------------
1 | [GTK]
2 | gtk-theme-name=Mint-Y-Dark
3 | gtk-application-prefer-dark-theme=true
4 | gtk-cursor-theme-name=Adwaita
5 |
6 | [Greeter]
7 | ui-file-location=INSTALL_PATH/share/elephant-greeter/elephant-greeter.ui
8 | x-icon-location=INSTALL_PATH/share/elephant-greeter/img/X.png
9 | wayland-icon-location=INSTALL_PATH/share/elephant-greeter/img/wayland.png
10 |
11 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | INSTALL_PATH=/usr/local
2 | CONFIG_PATH=/etc
3 | PKG_PREFIX=
4 |
5 | elephant-greeter.conf: elephant-greeter.conf.base
6 | sed -e "s|INSTALL_PATH|$(INSTALL_PATH)|" elephant-greeter.conf.base > elephant-greeter.conf
7 |
8 | clean:
9 | rm elephant-greeter.conf
10 |
11 | install: elephant-greeter.conf
12 | install -D -m 644 -t $(PKG_PREFIX)$(CONFIG_PATH)/lightdm/ elephant-greeter.conf
13 | install -D -m 755 -t $(PKG_PREFIX)$(INSTALL_PATH)/bin elephant-greeter.py
14 | install -D -m 644 -t $(PKG_PREFIX)$(INSTALL_PATH)/share/lightdm/greeters lightdm-elephant-greeter.desktop lightdm-elephant-greeter-x11.desktop
15 | install -D -m 644 -t $(PKG_PREFIX)$(INSTALL_PATH)/share/elephant-greeter elephant-greeter.ui
16 | install -D -m 644 -t $(PKG_PREFIX)$(INSTALL_PATH)/share/elephant-greeter/img img/*
17 |
18 | uninstall:
19 | rm $(INSTALL_PATH)/bin/elephant-greeter.py
20 | rm -r $(INSTALL_PATH)/share/elephant-greeter/
21 | rm $(INSTALL_PATH)/share/lightdm/greeters/lightdm-elephant-greeter.desktop
22 | rm $(INSTALL_PATH)/share/lightdm/greeters/lightdm-elephant-greeter-x11.desktop
23 | rm $(CONFIG_PATH)/lightdm/elephant-greeter.conf
24 |
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Maximilian Moser
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/img/X.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LightDM Elephant Greeter
2 |
3 | A small and simple [LightDM](https://github.com/canonical/lightdm) greeter using Python and GTK that doesn't require an X11 server.
4 |
5 | It is based on [Matt ~~Shultz's~~ Fischer's example LightDM greeter](http://www.mattfischer.com/blog/archives/5).
6 |
7 |
8 | ## Screenshot
9 |
10 | 
11 |
12 |
13 | ## Features
14 |
15 | * optionally uses Wayland, via [Cage](https://www.hjdskes.nl/projects/cage/) (instead of X11)
16 | * remembers the last authenticated user
17 | * automatically selects the last used session per user
18 |
19 | **Note**: The last authenticated user is stored in a cache file in the LightDM user's home directory (e.g. `/var/lib/lightdm/.cache/elephant-greeter/state`), similar to [Slick Greeter](https://github.com/linuxmint/slick-greeter/blob/ae927483c5dcf3ae898b3f0849e3770cfa04afa1/src/user-list.vala#L1026).
20 |
21 |
22 | ## Requirements
23 |
24 | * LightDM
25 | * Python 3.8+
26 | * [PyGObject](https://pygobject.readthedocs.io/en/latest/index.html): GObject bindings for Python
27 | * [Cage](https://www.hjdskes.nl/projects/cage/): small wayland compositor for the greeter
28 |
29 | **Note**: Please make sure you have all requirements installed, as having a LightDM greeter constantly failing isn't as much fun as it sounds.
30 |
31 |
32 | ## Installation
33 |
34 | The greeter can be installed by copying the files to the right places (`make install`) and updating LightDM's configuration file to register the greeter (`/etc/lightdm/lightdm.conf`):
35 | ```ini
36 | [LightDM]
37 | sessions-directory=/usr/share/lightdm/sessions:/usr/share/wayland-sessions:/usr/share/xsessions
38 | greeters-directory=/usr/local/share/lightdm/greeters:/usr/share/xgreeters
39 |
40 | [Seat:*]
41 | greeter-session=lightdm-elephant-greeter
42 | ```
43 |
44 | **Note**: If you wish to install the files somewhere else, specify them in the `make` command.
45 | For instance, to install the files into subdirectories of `/usr` instead of `/usr/local`, call `make INSTALL_PATH=/usr install`.
46 | The `CONFIG_PATH` (default: `/etc`) can be overridden in the same fashion.
47 |
48 |
49 | ## Configuration
50 |
51 | The greeter's configuration file (`/etc/lightdm/elephant-greeter.conf`) contains the sections `Greeter` and `GTK`.
52 | The former are basic configuration values that can determine the behavior of the greeter (e.g. override file locations), while the latter are passed directly to GTK (and can be used to e.g. set the GTK theme).
53 |
54 | Example configuration file:
55 | ```ini
56 | [GTK]
57 | gtk-theme-name=Nordic
58 | gtk-application-prefer-dark-theme=true
59 |
60 | [Greeter]
61 | default-session=sway
62 | ui-file-location=/usr/local/share/elephant-greeter/elephant-greeter.ui
63 | x-icon-location=/usr/local/share/elephant-greeter/img/X.png
64 | wayland-icon-location=/usr/local/share/elephant-greeter/img/wayland.png
65 | ```
66 |
67 |
68 | ## Additional Notes
69 |
70 | This project used to be called `max-moser-greeter`, until I could finally come up with a better name.
71 |
72 |
73 | ## Licenses
74 |
75 | * `img/X.svg`: [CC-BY-SA, by Sven](https://commons.wikimedia.org/wiki/File:X.Org\_Logo.svg)
76 | * `img/wayland.svg`: [CC-BY, by Kristian Høgsberg](https://commons.wikimedia.org/wiki/File:Wayland\_Logo.svg)
77 |
78 |
--------------------------------------------------------------------------------
/img/wayland.svg:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
--------------------------------------------------------------------------------
/elephant-greeter.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
230 |
231 |
--------------------------------------------------------------------------------
/elephant-greeter.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | #
3 | # Simple LightDM greeter, based on GTK 3.
4 | #
5 | # The code is based on the example greeter written and explained by
6 | # Matt Fischer:
7 | # http://www.mattfischer.com/blog/archives/5
8 |
9 | import configparser
10 | import gi
11 | import os
12 | import sys
13 | from pathlib import Path
14 |
15 | gi.require_version("Gtk", "3.0")
16 | gi.require_version("Gdk", "3.0")
17 | gi.require_version("LightDM", "1")
18 |
19 | from gi.repository import GLib
20 | from gi.repository import Gtk
21 | from gi.repository import Gdk
22 | from gi.repository import GdkPixbuf
23 | from gi.repository import LightDM
24 |
25 | DEFAULT_SESSION = "sway"
26 | UI_FILE_LOCATION = "/usr/local/share/elephant-greeter/elephant-greeter.ui"
27 | WAYLAND_ICON_LOCATION = "/usr/local/share/elephant-greeter/img/wayland.png"
28 | X_ICON_LOCATION = "/usr/local/share/elephant-greeter/img/X.png"
29 |
30 | # read the cache
31 | cache_dir = (Path.home() / ".cache" / "elephant-greeter")
32 | cache_dir.mkdir(parents=True, exist_ok=True)
33 | state_file = (cache_dir / "state")
34 | state_file.touch()
35 | cache = configparser.ConfigParser()
36 | cache.read(str(state_file))
37 | if not cache.has_section("greeter"):
38 | cache.add_section("greeter")
39 |
40 | greeter = None
41 | password_entry = None
42 | message_label = None
43 | login_clicked = False
44 |
45 |
46 | def set_password_visibility(visible):
47 | """Show or hide the password entry field."""
48 | password_entry.set_sensitive(visible)
49 | password_label.set_sensitive(visible)
50 | if visible:
51 | password_entry.show()
52 | password_label.show()
53 | else:
54 | password_entry.hide()
55 | password_label.hide()
56 |
57 |
58 | def read_config(gtk_settings, config_file="/etc/lightdm/elephant-greeter.conf"):
59 | """Read the configuration from the file."""
60 | if not os.path.isfile(config_file):
61 | return
62 |
63 | config = configparser.ConfigParser()
64 | config.read(config_file)
65 | if "GTK" in config:
66 | # every setting in the GTK section starting with 'gtk-' is applied directly
67 | for key in config["GTK"]:
68 | if key.startswith("gtk-"):
69 | value = config["GTK"][key]
70 | gtk_settings.set_property(key, value)
71 |
72 | if "Greeter" in config:
73 | global DEFAULT_SESSION, UI_FILE_LOCATION, X_ICON_LOCATION, WAYLAND_ICON_LOCATION
74 | DEFAULT_SESSION = config["Greeter"].get("default-session", DEFAULT_SESSION)
75 | UI_FILE_LOCATION = config["Greeter"].get("ui-file-location", UI_FILE_LOCATION)
76 | X_ICON_LOCATION = config["Greeter"].get("x-icon-location", X_ICON_LOCATION)
77 | WAYLAND_ICON_LOCATION = config["Greeter"].get("wayland-icon-location", WAYLAND_ICON_LOCATION)
78 |
79 |
80 | def write_cache():
81 | """Write the current cache to file."""
82 | with open(str(state_file), "w") as file_:
83 | cache.write(file_)
84 |
85 |
86 | def auto_select_user_session(username):
87 | """Automatically select the user's preferred session."""
88 | users = LightDM.UserList().get_users()
89 | users = [u for u in users if u.get_name() == username] + [None]
90 | user = users[0]
91 |
92 | if user is not None:
93 | session_index = 0
94 | if user.get_session() is not None:
95 | # find the index of the user's session in the combobox
96 | session_index = [row[0] for row in sessions_box.get_model()].index(user.get_session())
97 |
98 | sessions_box.set_active(session_index)
99 |
100 |
101 | def start_session():
102 | session = sessions_box.get_active_text() or DEFAULT_SESSION
103 | write_cache()
104 | if not greeter.start_session_sync(session):
105 | print("failed to start session", file=sys.stderr)
106 | message_label.set_text("Failed to start Session")
107 |
108 |
109 | def dm_show_prompt_cb(greeter, text, prompt_type=None, **kwargs):
110 | """Respond to the password request sent by LightDM."""
111 | # this event is sent by LightDM after user authentication
112 | # started, if a password is required
113 | if login_clicked:
114 | greeter.respond(password_entry.get_text())
115 | password_entry.set_text("")
116 |
117 | if "password" not in text.lower():
118 | print(f"LightDM requested prompt: {text}", file=sys.stderr)
119 |
120 |
121 | def dm_show_message_cb(greeter, text, message_type=None, **kwargs):
122 | """Show the message from LightDM to the user."""
123 | print(f"message from LightDM: {text}", file=sys.stderr)
124 | message_label.set_text(text)
125 |
126 |
127 | def dm_authentication_complete_cb(greeter):
128 | """Handle the notification that the authentication is completed."""
129 | if not login_clicked:
130 | # if this callback is executed before we clicked the login button,
131 | # this means that this user doesn't require a password
132 | # - in this case, we hide the password entry
133 | set_password_visibility(False)
134 |
135 | else:
136 | if greeter.get_is_authenticated():
137 | # the user authenticated successfully:
138 | # try to start the session
139 | start_session()
140 | else:
141 | # autentication complete, but unsucessful:
142 | # likely, the password was wrong
143 | message_label.set_text("Login failed")
144 | print("login failed", file=sys.stderr)
145 |
146 |
147 | def user_change_handler(widget, data=None):
148 | """Event handler for selecting a different username in the ComboBox."""
149 | global login_clicked
150 | login_clicked = False
151 |
152 | if greeter.get_in_authentication():
153 | greeter.cancel_authentication()
154 |
155 | username = usernames_box.get_active_text()
156 | greeter.authenticate(username)
157 | auto_select_user_session(username)
158 |
159 | set_password_visibility(True)
160 | password_entry.set_text("")
161 | cache.set("greeter", "last-user", username)
162 |
163 |
164 | def login_click_handler(widget, data=None):
165 | """Event handler for clicking the Login button."""
166 | global login_clicked
167 | login_clicked = True
168 |
169 | if greeter.get_is_authenticated():
170 | # the user is already authenticated:
171 | # this is likely the case when the user doesn't require a password
172 | start_session()
173 |
174 | if greeter.get_in_authentication():
175 | # if we're in the middle of an authentication, let's cancel it
176 | greeter.cancel_authentication()
177 |
178 | # (re-)start the authentication for the selected user
179 | # this should trigger LightDM to send a 'show-prompt' signal
180 | # (note that this time, login_clicked is True, however)
181 | username = usernames_box.get_active_text()
182 | greeter.authenticate(username)
183 |
184 |
185 | def poweroff_click_handler(widget, data=None):
186 | """Event handler for clicking the Power-Off button."""
187 | if LightDM.get_can_shutdown():
188 | LightDM.shutdown()
189 |
190 |
191 | if __name__ == "__main__":
192 | builder = Gtk.Builder()
193 | greeter = LightDM.Greeter()
194 | settings = Gtk.Settings.get_default()
195 | read_config(settings)
196 | cursor = Gdk.Cursor(Gdk.CursorType.LEFT_PTR)
197 | greeter_session_type = os.environ.get("XDG_SESSION_TYPE", None)
198 |
199 | # connect signal handlers to LightDM
200 | # signals: http://people.ubuntu.com/~robert-ancell/lightdm/reference/LightDMGreeter.html#LightDMGreeter-authentication-complete
201 | greeter.connect("authentication-complete", dm_authentication_complete_cb)
202 | greeter.connect("show-message", dm_show_message_cb)
203 | greeter.connect("show-prompt", dm_show_prompt_cb)
204 |
205 | # connect builder and widgets
206 | ui_file_path = UI_FILE_LOCATION
207 | builder.add_from_file(ui_file_path)
208 | login_window = builder.get_object("login_window")
209 | password_entry = builder.get_object("password_entry")
210 | password_label = builder.get_object("password_label")
211 | message_label = builder.get_object("message_label")
212 | usernames_box = builder.get_object("usernames_cb")
213 | sessions_box = builder.get_object("sessions_cb")
214 | login_button = builder.get_object("login_button")
215 | poweroff_button = builder.get_object("poweroff_button")
216 | icon = builder.get_object("icon")
217 |
218 | # connect to greeter
219 | greeter.connect_to_daemon_sync()
220 |
221 | # set up the GUI
222 | login_window.get_root_window().set_cursor(cursor)
223 | password_entry.set_text("")
224 | password_entry.set_sensitive(True)
225 | password_entry.set_visibility(False)
226 | if greeter_session_type is not None:
227 | print(f"greeter session type: {greeter_session_type}", file=sys.stderr)
228 | message_label.set_text("Welcome Back!")
229 | if greeter_session_type.lower() == "wayland":
230 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(WAYLAND_ICON_LOCATION, 32, 32, False)
231 | icon.set_from_pixbuf(pixbuf)
232 | elif greeter_session_type.lower() == "x11":
233 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(X_ICON_LOCATION, 32, 32, False)
234 | icon.set_from_pixbuf(pixbuf)
235 |
236 | # register handlers for our UI elements
237 | poweroff_button.connect("clicked", poweroff_click_handler)
238 | usernames_box.connect("changed", user_change_handler)
239 | password_entry.connect("activate", login_click_handler)
240 | login_button.connect("clicked", login_click_handler)
241 | login_window.set_default(login_button)
242 |
243 | # make the greeter "fullscreen"
244 | screen = login_window.get_screen()
245 | login_window.resize(screen.get_width(), screen.get_height())
246 |
247 | # populate the combo boxes
248 | user_idx = 0
249 | last_user = cache.get("greeter", "last-user", fallback=None)
250 | for idx, user in enumerate(LightDM.UserList().get_users()):
251 | usernames_box.append_text(user.get_name())
252 | if last_user == user.get_name():
253 | user_idx = idx
254 |
255 | for session in LightDM.get_sessions():
256 | sessions_box.append_text(session.get_key())
257 |
258 | sessions_box.set_active(0)
259 | usernames_box.set_active(user_idx)
260 |
261 | # if the selected user requires a password, (i.e the password entry
262 | # is visible), focus the password entry -- otherwise, focus the
263 | # user selection box
264 | if password_entry.get_sensitive():
265 | password_entry.grab_focus()
266 | else:
267 | usernames_box.grab_focus()
268 |
269 | login_window.show()
270 | login_window.fullscreen()
271 | GLib.MainLoop().run()
272 |
--------------------------------------------------------------------------------