├── .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 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /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 | ![Screenshot](./screenshot.png?raw=true "Screenshot") 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 | 26 | 27 | 28 | 29 | 30 | 31 | image/svg+xml 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /elephant-greeter.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | True 7 | False 8 | -1 9 | -1 10 | center 11 | 12 | 13 | 500 14 | True 15 | False 16 | center 17 | center 18 | vertical 19 | 10 20 | 21 | 22 | True 23 | False 24 | 25 | 26 | True 27 | False 28 | img/wayland.svg 29 | 30 | 31 | False 32 | True 33 | 0 34 | 35 | 36 | 37 | 38 | True 39 | False 40 | 50 41 | 50 42 | [message] 43 | 44 | 45 | 46 | 47 | 48 | True 49 | True 50 | 1 51 | 52 | 53 | 54 | 55 | False 56 | True 57 | 0 58 | 59 | 60 | 61 | 62 | True 63 | False 64 | 25 65 | 66 | 67 | 100 68 | True 69 | False 70 | 10 71 | Session: 72 | 1 73 | 74 | 75 | False 76 | True 77 | 0 78 | 79 | 80 | 81 | 82 | True 83 | False 84 | True 85 | 86 | 87 | True 88 | True 89 | 2 90 | 91 | 92 | 93 | 94 | False 95 | True 96 | 1 97 | 98 | 99 | 100 | 101 | True 102 | False 103 | 104 | 105 | 100 106 | True 107 | False 108 | 10 109 | User: 110 | 1 111 | 112 | 113 | False 114 | True 115 | 0 116 | 117 | 118 | 119 | 120 | True 121 | False 122 | True 123 | 124 | 125 | True 126 | True 127 | 2 128 | 129 | 130 | 131 | 132 | False 133 | True 134 | 3 135 | 136 | 137 | 138 | 139 | True 140 | False 141 | 142 | 143 | 100 144 | True 145 | False 146 | 10 147 | Password: 148 | 1 149 | 150 | 151 | False 152 | True 153 | 0 154 | 155 | 156 | 157 | 158 | True 159 | True 160 | True 161 | False 162 | password 163 | 164 | 165 | True 166 | True 167 | 1 168 | 169 | 170 | 171 | 172 | False 173 | True 174 | 3 175 | 176 | 177 | 178 | 179 | True 180 | False 181 | center 182 | 25 183 | 20 184 | 185 | 186 | Login 187 | False 188 | 100 189 | True 190 | True 191 | True 192 | center 193 | 194 | 197 | 198 | 199 | False 200 | False 201 | 1 202 | 203 | 204 | 205 | 206 | Power Off 207 | True 208 | True 209 | True 210 | 213 | 214 | 215 | False 216 | True 217 | 2 218 | 219 | 220 | 221 | 222 | False 223 | True 224 | 5 225 | 226 | 227 | 228 | 229 | 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 | --------------------------------------------------------------------------------