├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README ├── README.md ├── cache.json ├── hyprland.conf ├── img ├── avatar-default.svg ├── poweroff-default.svg ├── reboot-default.svg └── sleep-default.svg ├── install.sh ├── nwg-hello-default.css ├── nwg-hello-default.json ├── nwg.jpg ├── nwg_hello ├── __about__.py ├── __init__.py ├── langs │ ├── cs_CZ │ ├── de_AT │ ├── de_DE │ ├── en_US │ ├── es_AR │ ├── es_ES │ ├── fr_FR │ ├── it_IT │ ├── ja_JP │ ├── pl_PL │ ├── pt_BR │ ├── pt_PT │ ├── ru_RU │ ├── tr_TR │ └── zh_CN ├── main.py ├── template.glade ├── tools.py └── ui.py ├── setup.py ├── sway-config └── uninstall.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: nwg-piotr 2 | liberapay: nwg 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /venv 3 | /nwg_hello.egg-info/ 4 | /build/ 5 | /dist/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Piotr Miller 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 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Do not edit *-default.* files. To define own settings and the style sheet, copy files to: 2 | 3 | # cp nwg-hello-default.json nwg-hello.json 4 | # cp nwg-hello-default.css nwg-hello.css 5 | 6 | and edit copies. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nwg-hello 2 | 3 | This program is a part of the [nwg-shell](https://nwg-piotr.github.io/nwg-shell) project. 4 | 5 | Nwg-hello is a GTK3-based greeter for the [greetd](https://git.sr.ht/~kennylevinsen/greetd) daemon, written in python. 6 | It is meant to work under a Wayland compositor, like [sway](https://swaywm.org) or [Hyprland](https://hyprland.org) (also see: 7 | [Running on Debian and labwc](#running-on-debian-and-labwc)). 8 | The greeter has been developed for the [nwg-iso](https://github.com/nwg-piotr/nwg-iso) project, but it may be configured for standalone use. 9 | 10 | Screenshot
11 | 12 | _The screen layout is heavily inspired by [Sugar Candy SDDM theme](https://framagit.org/MarianArlt/sddm-sugar-candy) 13 | by Marian Arlt._ 14 | 15 | ## Background 16 | 17 | I was in need of a good login manager for the nwg-iso project. I first used SDDM with the Sugar Candy theme, and it 18 | worked pretty well. However, it brings QT dependencies, and my stuff is all GTK-based. Also, I know nothing about the QT 19 | framework, so couldn't adjust the greeter to my taste. The next choice was LightDM with [my modification](https://github.com/nwg-piotr/lightdm-nwg-greeter) 20 | of the [LightDM Elephant Greeter](https://github.com/max-moser/lightdm-elephant-greeter) by Maximilian Moser. It looked well, but LightDM would happen to hang way too 21 | often. Then I gave a try to greetd, and that was it. I only needed a nice graphical greeter based on GTK3. Since there 22 | was no such thing, I had to develop one. 23 | 24 | ## Features 25 | 26 | - Multi-monitor support with gtk-layer-shell; 27 | - multi-language support; 28 | - background and style customization with CSS; 29 | - automatic selection of the last used session and user; 30 | - avatar (user picture) support; 31 | - support for setting environment variables. 32 | 33 | ## Dependencies 34 | 35 | - python >= 3.6; 36 | - greetd; 37 | - gtk3; 38 | - gtk-layer-shell; 39 | - Hyprland or sway Wayland compositor; 40 | - gnome-themes-extra. 41 | 42 | ## Make dependencies 43 | 44 | - python-build 45 | - python-installer 46 | - python-wheel 47 | - python-setuptools 48 | 49 | ## Optional dependencies 50 | 51 | - mugshot: to set the user picture 52 | 53 | ## Installation 54 | 55 | [![Packaging status](https://repology.org/badge/vertical-allrepos/nwg-hello.svg)](https://repology.org/project/nwg-hello/versions) 56 | 57 | First, you need to [install and start greetd](https://wiki.archlinux.org/title/Greetd#Installation). 58 | 59 | The greeter can be installed as a package for your Linux distribution, or by cloning this repository and executing the 60 | `install.sh` script (make sure you installed dependencies first). 61 | 62 | Then you need to edit the `/etc/greetd/config.toml` file (or `greetd.conf` - see the tip below). 63 | 64 | ```toml 65 | [terminal] 66 | # The VT to run the greeter on. Can be "next", "current" or a number 67 | # designating the VT. 68 | vt = 1 69 | 70 | # The default session, also known as the greeter. 71 | [default_session] 72 | 73 | # `agreety` is the bundled agetty/login-lookalike. You can replace `/bin/sh` 74 | # with whatever you want started, such as `sway`. 75 | command = "agreety --cmd /bin/sh" 76 | 77 | # The user to run the command as. The privileges this user must have depends 78 | # on the greeter. A graphical greeter may for example require the user to be 79 | # in the `video` group. 80 | user = "greeter" 81 | ``` 82 | 83 | Replace the line: 84 | 85 | ```toml 86 | command = "agreety --cmd /bin/sh" 87 | ``` 88 | 89 | with 90 | 91 | ```toml 92 | command = "Hyprland -c /etc/nwg-hello/hyprland.conf" 93 | ``` 94 | 95 | if you want to use Hyprland, or this line if you prefer sway: 96 | 97 | ```toml 98 | command = "sway -c /etc/nwg-hello/sway-config" 99 | ``` 100 | 101 | NOTE: you may need `sway --unsupported-gpu` for Nvidia. Also, if you'd like to make some additional configuration 102 | (e.g., monitor layout), edit `/etc/nwg-hello/hyprland.conf` or `/etc/nwg-hello/sway-config`, respectively. 103 | 104 | __Do not change the__ `user = "greeter"` __line__, or some file-related functions won't work. 105 | 106 | ### Tip 107 | 108 | During the greetd package upgrades, the `config.toml` file may be overwritten with the default one. E.g., on Arch Linux 109 | your modified file gets renamed to `config.toml.pacsave`. This will restore the `agreety` greeter on your system. 110 | To avoid such a situation, you may use the alternative `greetd.conf` file. This has not been mentioned in docs, 111 | but greetd looks for this file first. Just copy `config.toml` to `greetd.conf` and make changes to the copy. 112 | 113 | ## Configuration 114 | 115 | Copy `/etc/nwg-hello/nwg-hello-default.json` to `/etc/nwg-hello/nwg-hello.json` and make your changes there. 116 | 117 | ```json 118 | { 119 | "session_dirs": [ 120 | "/usr/share/wayland-sessions", 121 | "/usr/share/xsessions" 122 | ], 123 | "custom_sessions": [ 124 | { 125 | "name": "Shell", 126 | "exec": "/usr/bin/bash" 127 | } 128 | ], 129 | "monitor_nums": [], 130 | "form_on_monitors": [], 131 | "delay_secs": 1, 132 | "cmd-sleep": "systemctl suspend", 133 | "cmd-reboot": "systemctl reboot", 134 | "cmd-poweroff": "systemctl poweroff", 135 | "gtk-theme": "Adwaita", 136 | "gtk-icon-theme": "", 137 | "gtk-cursor-theme": "", 138 | "prefer-dark-theme": true, 139 | "template-name": "", 140 | "time-format": "%H:%M:%S", 141 | "date-format": "%A, %d. %B", 142 | "layer": "overlay", 143 | "keyboard-mode": "exclusive", 144 | "lang": "", 145 | "avatar-show": false, 146 | "avatar-size": 100, 147 | "avatar-border-width": 1, 148 | "avatar-border-color": "#eee", 149 | "avatar-corner-radius": 15, 150 | "avatar-circle": false, 151 | "env-vars": [] 152 | } 153 | ``` 154 | 155 | - `"session_dirs"`: comma-separated paths to session directories. Modify only if you know well what you're doing. 156 | - `"custom_sessions"`: greetd can run whatever starts from the command line. This way we can add `bash`, `zsh` or something else here. The `"name"` field is the display name. The `"exec"` field is a command. 157 | - `"monitor_nums"`: leave as is to see the greeter on all monitors. Set e.g. `[0, 2]` for it to appear on the 1st and 3rd one. 158 | - `"form_on_monitors"`: which of above monitors to display the login form on (just the wallpaper on the rest). 159 | - `"delay_secs"`: some monitors take longer to turn on. In the meantime the greeter may behave oddly on other monitors. If it happens to restart/blink, increase this value. If you only have one monitor and no discrete GPU, you may probably set `0` here. 160 | - `"cmd-sleep"`, `"cmd-reboot"`, and `"cmd-poweroff"` are pre-defined for systemd-based systems. Use whatever works for you. 161 | - `"gtk-theme"`, `"gtk-icon-theme"` and `"gtk-cursor-theme"` are of little importance as long, as you use classes and IDs from the default css style sheet. 162 | - `"prefer-dark-theme"` should remain `true`, unless you need to turn it off for use with your own background and/or styling. 163 | - `"template-name"` allows use of own templates: find the built-in `/usr/lib/python3.xx/site-packages/nwg_hello-x.y.z-py3.xx.egg/nwg_hello/template.glade` file, copy to a folder somewhere in `~/`, edit and place as `/etc/nwg-hello/file-name.glade`. Do not change widget IDs. Set your `file-name.glade` as the `"template-name"` value. Leave blank to use the built-in template. 164 | - `"time-format"`: string to format clock with the strftime() function (see: https://www.man7.org/linux/man-pages/man3/strftime.3.html). 165 | - `"date-format"`: string to format date with the strftime() function (see: https://www.man7.org/linux/man-pages/man3/strftime.3.html). 166 | - `"layer"`: allows choosing gtk-layer-shell layer: 'background', 'bottom', 'top', 'overlay'; 'overlay' will be used if no value given. 167 | - `"keyboard-mode"`: allows choosing gtk-layer-shell keyboard mode: 'none', 'exclusive', 'on_demand'; 'exclusive' will be used if no value given. 168 | - `"lang"` allows you to force the use of a specific language, regardless of the `$LANG` system variable. Check if we have the translation in the [langs directory](https://github.com/nwg-piotr/nwg-hello/tree/main/nwg_hello/langs). 169 | - `"avatar-show"`: determines whether to display the user's profile picture. 170 | - `"avatar-size"`: avatar image size in pixels. 171 | - `"avatar-border-width"`: avatar border width in pixels. 172 | - `"avatar-border-color"`: a hexadecimal value of avatar border color ("#rgb" or "#rrggbb"). 173 | - `"avatar-corner-radius"`: corner radius for rectangular avatar image, 174 | - `"avatar-circle"`: draw avatar as a circle (corner radius ignored), 175 | - `"env-vars"` allows to pass an array of environment variables. Use like this: `["MY_VAR=value", "OTHER_VAR=value1"]`. 176 | 177 | ## Styling 178 | 179 | Copy `/etc/nwg-hello/nwg-hello-default.css` to `/etc/nwg-hello/nwg-hello.css` and make your changes there. 180 | 181 | ## Custom icons 182 | 183 | If you'd like to use your own icons, do not replace `/usr/share/nwg-hello/*-default.svg` files. Place your 184 | `poweroff.svg`, `reboot.svg`, `sleep.svg` and `avatar.svg` files in the same directory. Attention: the `avatar.svg` file 185 | is not your profile picture, but a generic user image! 186 | 187 | ## Own language files 188 | 189 | You can't translate labels in the .glade file, as the program replaces the values with what's defined in 190 | [language files](https://github.com/nwg-piotr/nwg-hello/tree/main/nwg_hello/langs). Since the 0.2.4 version, however, 191 | you can copy your lang file to `/etc/nwg-hello/` and make desired changes there, 192 | see https://github.com/nwg-piotr/nwg-hello/issues/19. Be careful with syntax, the JSON format is unforgiving. 193 | Test your lang file by running `nwg-hello -t -d` from terminal. 194 | 195 | ## User avatar 196 | 197 | New in version 0.4.0. Disabled in default config. Set `"avatar-show": true` to enable. 198 | 199 | The feature displays user's profile picture from `/var/lib/AccountsService/icons/$USERNAME`, stored by 200 | gnome-control-center or some other tool ([Mugshot](https://github.com/bluesabre/mugshot) does the job perfectly well). See [Configuration](#configuration) 201 | for related values. 202 | 203 | Styling: no CSS is applicable here. You can set shape- and border-related values in config. 204 | 205 | NOTE: if you use your customized `/etc/nwg-hello/nwg-hello.json` file, remember to copy all `avatar-*` key-value pairs 206 | from `/etc/nwg-hello/nwg-hello-default.json`. 207 | 208 | ## Running on Debian and labwc 209 | 210 | Submitted by [@01micko](https://github.com/01micko). 211 | 212 | ### configs 213 | 214 | #### /etc/greetd/greetd.conf 215 | 216 | ```toml 217 | [terminal] 218 | # The VT to run the greeter on. Can be "next", "current" or a number 219 | # designating the VT. 220 | vt = 7 221 | 222 | # The default session, also known as the greeter. 223 | [default_session] 224 | 225 | command = "labwc --config-dir /etc/nwg-hello/labwc-config" 226 | 227 | # The user to run the command as. The privileges this user must have depends 228 | # on the greeter. A graphical greeter may for example require the user to be 229 | # in the `video` group. 230 | user = "_greetd" 231 | ``` 232 | 233 | NOTE: The user `_greetd` is a debian thing, even though it isn't set up correctly. You have to manually add to 'video' 234 | group and `chown` all the files in `/etc/greetd`. 235 | 236 | #### /etc/nwg-hello/labwc-config/autostart 237 | 238 | ```sh 239 | # start nwg-hello - full paths aren't required, but saves lookup time 240 | exec /usr/bin/nwg-hello; /usr/bin/labwc --exit 241 | ``` 242 | 243 | #### /etc/nwg-hello/labwc-config/rc.xml (optional) 244 | - only if you want a screenshot (using `PrtScr` key) of `nwg-hello` saved to /etc/greetd/ 245 | 246 | ```xml 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | ``` 257 | 258 | ## Acknowledgments 259 | 260 | - [@milisarge](https://gist.github.com/milisarge) for [the snippet](https://gist.github.com/milisarge/d169756e316e185572605699e73ed3ae) that let me know how things work; 261 | - [Marian Arlt](https://framagit.org/MarianArlt) for inspiring look of the Sugar Candy SDDM theme. 262 | -------------------------------------------------------------------------------- /cache.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /hyprland.conf: -------------------------------------------------------------------------------- 1 | monitor=,preferred,auto,1 2 | bind = ALT, Q, killactive, 3 | misc { 4 | disable_hyprland_logo = true 5 | } 6 | animations { 7 | enabled = false 8 | } 9 | exec-once = nwg-hello; hyprctl dispatch exit 10 | -------------------------------------------------------------------------------- /img/avatar-default.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 32 | 34 | 39 | 40 | 45 | 46 | -------------------------------------------------------------------------------- /img/poweroff-default.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 35 | 37 | 39 | 40 | 42 | image/svg+xml 43 | 45 | 46 | 47 | 48 | 52 | 53 | -------------------------------------------------------------------------------- /img/reboot-default.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 36 | 38 | 40 | 41 | 43 | image/svg+xml 44 | 46 | 47 | 48 | 49 | 53 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /img/sleep-default.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 35 | 37 | 39 | 40 | 42 | image/svg+xml 43 | 45 | 46 | 47 | 48 | 52 | 53 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Before running this script, make sure you have python-build, python-installer, 4 | # python-wheel and python-setuptools installed. 5 | 6 | PROGRAM_NAME="nwg-hello" 7 | MODULE_NAME="nwg_hello" 8 | SITE_PACKAGES="$(python3 -c "import sysconfig; print(sysconfig.get_paths()['purelib'])")" 9 | PATTERN="$SITE_PACKAGES/$MODULE_NAME*" 10 | 11 | # Remove from site_packages 12 | for path in $PATTERN; do 13 | if [ -e "$path" ]; then 14 | echo "Removing $path" 15 | rm -r "$path" 16 | fi 17 | done 18 | 19 | [ -d "./dist" ] && rm -rf ./dist 20 | 21 | # Remove launcher scripts 22 | filenames=("/usr/bin/$PROGRAM_NAME") 23 | 24 | for filename in "${filenames[@]}"; do 25 | rm -f "$filename" 26 | echo "Removing -f $filename" 27 | done 28 | 29 | python -m build --wheel --no-isolation 30 | 31 | python -m installer dist/*.whl 32 | 33 | install -D -m 644 -t /etc/nwg-hello/ nwg-hello-default.json 34 | install -D -m 644 -t /etc/nwg-hello/ nwg-hello-default.css 35 | install -D -m 644 -t /etc/nwg-hello/ hyprland.conf 36 | install -D -m 644 -t /etc/nwg-hello/ sway-config 37 | install -D -m 644 -t /etc/nwg-hello/ README 38 | install -D -m 644 -t /usr/share/nwg-hello/ nwg.jpg 39 | install -D -m 644 -t /usr/share/nwg-hello/ img/* 40 | 41 | install -d /var/cache/nwg-hello 42 | install -Dm644 -t /var/cache/nwg-hello cache.json -o greeter 43 | 44 | install -Dm 644 -t "/usr/share/licenses/$PROGRAM_NAME" LICENSE 45 | install -Dm 644 -t "/usr/share/doc/$PROGRAM_NAME" README.md 46 | -------------------------------------------------------------------------------- /nwg-hello-default.css: -------------------------------------------------------------------------------- 1 | window { 2 | background-image: url("/usr/share/nwg-hello/nwg.jpg"); background-size: auto 100% 3 | } 4 | 5 | #form-wrapper { 6 | background-color: rgba(0, 0, 0, 0.2) 7 | } 8 | 9 | entry { 10 | background-color: rgba(255, 255, 255, 0.1); 11 | border: 1px solid #eee; 12 | border-radius: 18px; 13 | padding: 12px 14 | } 15 | 16 | button { 17 | background: rgba(255, 255, 255, 0.1) none; 18 | border: 1px solid #eee; 19 | border-radius: 18px; 20 | padding: 12px 21 | } 22 | 23 | button:hover { 24 | background-color: rgba(255, 255, 255, 0.2) 25 | } 26 | 27 | #power-button { 28 | border-radius: 18px; 29 | background:none; 30 | border: none; 31 | } 32 | 33 | #power-button:hover { 34 | background-color: rgba(255, 255, 255, 0.1) 35 | } 36 | 37 | #power-button:active { 38 | background-color: rgba(255, 255, 0, 0.2) 39 | } 40 | 41 | #welcome-label { 42 | font-size: 48px 43 | } 44 | 45 | #clock-label { 46 | font-family: monospace; 47 | font-size: 30px 48 | } 49 | 50 | #date-label { 51 | font-size: 18px 52 | } 53 | 54 | #form-label { 55 | } 56 | 57 | #form-combo { 58 | } 59 | 60 | #password-entry { 61 | } 62 | 63 | #login-button { 64 | } 65 | -------------------------------------------------------------------------------- /nwg-hello-default.json: -------------------------------------------------------------------------------- 1 | { 2 | "session_dirs": [ 3 | "/usr/share/wayland-sessions", 4 | "/usr/share/xsessions" 5 | ], 6 | "custom_sessions": [ 7 | { 8 | "name": "Shell", 9 | "exec": "/usr/bin/bash" 10 | } 11 | ], 12 | "monitor_nums": [], 13 | "form_on_monitors": [], 14 | "delay_secs": 1, 15 | "cmd-sleep": "systemctl suspend", 16 | "cmd-reboot": "systemctl reboot", 17 | "cmd-poweroff": "systemctl poweroff", 18 | "gtk-theme": "Adwaita", 19 | "gtk-icon-theme": "", 20 | "gtk-cursor-theme": "", 21 | "prefer-dark-theme": true, 22 | "template-name": "", 23 | "time-format": "%H:%M:%S", 24 | "date-format": "%A, %d. %B", 25 | "layer": "overlay", 26 | "keyboard-mode": "exclusive", 27 | "lang": "", 28 | "avatar-show": false, 29 | "avatar-size": 100, 30 | "avatar-border-width": 1, 31 | "avatar-border-color": "#eee", 32 | "avatar-corner-radius": 15, 33 | "avatar-circle": false, 34 | "env-vars": [] 35 | } 36 | -------------------------------------------------------------------------------- /nwg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwg-piotr/nwg-hello/3a3cd1de4267e7870ec85e062f0ac5a99afd3ebb/nwg.jpg -------------------------------------------------------------------------------- /nwg_hello/__about__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from importlib import metadata 3 | except ImportError: 4 | import importlib_metadata as metadata 5 | 6 | try: 7 | __version__ = metadata.version("nwg-hello") 8 | except Exception: 9 | __version__ = "unknown" 10 | -------------------------------------------------------------------------------- /nwg_hello/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nwg-piotr/nwg-hello/3a3cd1de4267e7870ec85e062f0ac5a99afd3ebb/nwg_hello/__init__.py -------------------------------------------------------------------------------- /nwg_hello/langs/cs_CZ: -------------------------------------------------------------------------------- 1 | { 2 | "failed-starting-session": "Nepodařilo se spustit relaci", 3 | "login": "Přihlášení", 4 | "login-failed": "Chyba přihlášení", 5 | "password": "Heslo", 6 | "password-empty": "Heslo nemůže být prázdné", 7 | "power-off": "Vypnout", 8 | "session": "Relace", 9 | "show-password": "Zobrazit heslo", 10 | "sleep": "Uspat", 11 | "reboot": "Restartovat", 12 | "user": "Uživatel", 13 | "welcome": "Vítejte!" 14 | } 15 | -------------------------------------------------------------------------------- /nwg_hello/langs/de_AT: -------------------------------------------------------------------------------- 1 | { 2 | "failed-starting-session": "Sitzungsstart fehlgeschlagen", 3 | "login": "Anmeldung", 4 | "login-failed": "Fehler bei der Anmeldung", 5 | "password": "Passwort", 6 | "password-empty": "Passwort kann nicht leer sein", 7 | "power-off": "Ausschalten", 8 | "session": "Session", 9 | "show-password": "Passwort anzeigen", 10 | "sleep": "Schlafen", 11 | "reboot": "Neustart", 12 | "user": "Benutzer", 13 | "welcome": "Willkommen!" 14 | } -------------------------------------------------------------------------------- /nwg_hello/langs/de_DE: -------------------------------------------------------------------------------- 1 | { 2 | "failed-starting-session": "Sitzungsstart fehlgeschlagen", 3 | "login": "Anmeldung", 4 | "login-failed": "Fehler bei der Anmeldung", 5 | "password": "Passwort", 6 | "password-empty": "Passwort kann nicht leer sein", 7 | "power-off": "Ausschalten", 8 | "session": "Session", 9 | "show-password": "Passwort anzeigen", 10 | "sleep": "Schlafen", 11 | "reboot": "Neustart", 12 | "user": "Benutzer", 13 | "welcome": "Willkommen!" 14 | } -------------------------------------------------------------------------------- /nwg_hello/langs/en_US: -------------------------------------------------------------------------------- 1 | { 2 | "failed-starting-session": "Failed to start session", 3 | "login": "Login", 4 | "login-failed": "Login failed", 5 | "password": "Password", 6 | "password-empty": "Password cannot be empty", 7 | "power-off": "Power off", 8 | "session": "Session", 9 | "show-password": "Show password", 10 | "sleep": "Sleep", 11 | "reboot": "Reboot", 12 | "user": "User", 13 | "welcome": "Welcome!" 14 | } 15 | -------------------------------------------------------------------------------- /nwg_hello/langs/es_AR: -------------------------------------------------------------------------------- 1 | { 2 | "failed-starting-session": "No se pudo iniciar la sesión", 3 | "login": "Acceso", 4 | "login-failed": "Error de inicio de sesion", 5 | "password": "Contraseña", 6 | "password-empty": "La contraseña no puede estar vacía", 7 | "power-off": "Apagado", 8 | "session": "Sesión", 9 | "show-password": "Mostrar contraseña", 10 | "sleep": "Dormir", 11 | "reboot": "Reiniciar", 12 | "user": "Usuario", 13 | "welcome": "Bienvenido!" 14 | } -------------------------------------------------------------------------------- /nwg_hello/langs/es_ES: -------------------------------------------------------------------------------- 1 | { 2 | "failed-starting-session": "No se pudo iniciar la sesión", 3 | "login": "Acceso", 4 | "login-failed": "Error de inicio de sesion", 5 | "password": "Contraseña", 6 | "password-empty": "La contraseña no puede estar vacía", 7 | "power-off": "Apagado", 8 | "session": "Sesión", 9 | "show-password": "Mostrar contraseña", 10 | "sleep": "Dormir", 11 | "reboot": "Reiniciar", 12 | "user": "Usuario", 13 | "welcome": "Bienvenido!" 14 | } -------------------------------------------------------------------------------- /nwg_hello/langs/fr_FR: -------------------------------------------------------------------------------- 1 | { 2 | "failed-starting-session": "Échec du démarrage de la session", 3 | "login": "Se connecter", 4 | "login-failed": "La connexion a échoué", 5 | "password": "Mot de passe", 6 | "password-empty": "Le mot de passe ne peut pas être vide", 7 | "power-off": "Éteindre", 8 | "session": "Session", 9 | "show-password": "Montrer le mot de passe", 10 | "sleep": "Dormir", 11 | "reboot": "Redémarrer", 12 | "user": "Utilisateur", 13 | "welcome": "Bienvenu!" 14 | } -------------------------------------------------------------------------------- /nwg_hello/langs/it_IT: -------------------------------------------------------------------------------- 1 | { 2 | "failed-starting-session": "Impossibile avviare la sessione", 3 | "login": "Login", 4 | "login-failed": "Accesso non riuscito", 5 | "password": "Parola d'ordine", 6 | "password-empty": "La password non può essere vuota", 7 | "power-off": "Spegni", 8 | "session": "Sessione", 9 | "show-password": "Mostra password", 10 | "sleep": "Sonno", 11 | "reboot": "Riavviare", 12 | "user": "Utente", 13 | "welcome": "Benvenuto!" 14 | } -------------------------------------------------------------------------------- /nwg_hello/langs/ja_JP: -------------------------------------------------------------------------------- 1 | { 2 | "failed-starting-session": "セッションの開始に失敗しました", 3 | "login": "ログイン", 4 | "login-failed": "ログインに失敗しました", 5 | "password": "パスワード", 6 | "password-empty": "パスワードを空にすることはできません", 7 | "power-off": "電源オフ", 8 | "session": "セッション", 9 | "show-password": "パスワードを表示", 10 | "sleep": "スリープ", 11 | "reboot": "再起動", 12 | "user": "ユーザー", 13 | "welcome": "ようこそ!" 14 | } 15 | -------------------------------------------------------------------------------- /nwg_hello/langs/pl_PL: -------------------------------------------------------------------------------- 1 | { 2 | "failed-starting-session": "Nie udało się rozpocząć sesji", 3 | "login": "Zaloguj", 4 | "login-failed": "Logowanie nieudane", 5 | "password": "Hasło", 6 | "password-empty": "Hasło nie może być puste", 7 | "power-off": "Wyłącz", 8 | "session": "Sesja", 9 | "show-password": "Pokaż hasło", 10 | "sleep": "Uśpij", 11 | "reboot": "Restart", 12 | "user": "Użytkownik", 13 | "welcome": "Witaj!" 14 | } -------------------------------------------------------------------------------- /nwg_hello/langs/pt_BR: -------------------------------------------------------------------------------- 1 | { 2 | "failed-starting-session": "Falha ao iniciar sessão", 3 | "login": "Login", 4 | "login-failed": "Falha no login", 5 | "password": "Senha", 6 | "password-empty": "A senha não pode ficar vazia", 7 | "power-off": "Desligar", 8 | "session": "Sessão", 9 | "show-password": "Mostrar senha", 10 | "sleep": "Dormir", 11 | "reboot": "Reinício", 12 | "user": "Do utilizador", 13 | "welcome": "Bem-vindo!" 14 | } -------------------------------------------------------------------------------- /nwg_hello/langs/pt_PT: -------------------------------------------------------------------------------- 1 | { 2 | "failed-starting-session": "Falha ao iniciar sessão", 3 | "login": "Login", 4 | "login-failed": "Falha no login", 5 | "password": "Senha", 6 | "password-empty": "A senha não pode ficar vazia", 7 | "power-off": "Desligar", 8 | "session": "Sessão", 9 | "show-password": "Mostrar senha", 10 | "sleep": "Dormir", 11 | "reboot": "Reinício", 12 | "user": "Do utilizador", 13 | "welcome": "Bem-vindo!" 14 | } -------------------------------------------------------------------------------- /nwg_hello/langs/ru_RU: -------------------------------------------------------------------------------- 1 | { 2 | "failed-starting-session": "Не удалось начать сеанс", 3 | "login": "Войти", 4 | "login-failed": "Ошибка входа", 5 | "password": "Пароль", 6 | "password-empty": "Пароль не может быть пустым", 7 | "power-off": "Выключение", 8 | "session": "Сессия", 9 | "show-password": "Показать пароль", 10 | "sleep": "Усыпить", 11 | "reboot": "Перезагрузить", 12 | "user": "Пользователь", 13 | "welcome": "Привет!" 14 | } -------------------------------------------------------------------------------- /nwg_hello/langs/tr_TR: -------------------------------------------------------------------------------- 1 | { 2 | "failed-starting-session": "Oturum başlatılamadı", 3 | "login": "Giriş yapmak", 4 | "login-failed": "Giriş başarısız oldu", 5 | "password": "Şifre", 6 | "password-empty": "Şifre boş olamaz", 7 | "power-off": "Kapat", 8 | "session": "Oturum", 9 | "show-password": "Şifreyi göster", 10 | "sleep": "Uyumak", 11 | "reboot": "Yeniden başlat", 12 | "user": "Kullanıcı", 13 | "welcome": "Merhaba!" 14 | } -------------------------------------------------------------------------------- /nwg_hello/langs/zh_CN: -------------------------------------------------------------------------------- 1 | { 2 | "failed-starting-session": "无法开始会话", 3 | "login": "登录", 4 | "login-failed": "登录失败", 5 | "password": "密码", 6 | "password-empty": "密码不能为空", 7 | "power-off": "关机", 8 | "session": "会话", 9 | "show-password": "显示密码", 10 | "sleep": "休眠", 11 | "reboot": "重启", 12 | "user": "用户", 13 | "welcome": "欢迎!" 14 | } 15 | -------------------------------------------------------------------------------- /nwg_hello/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import os.path 4 | import time 5 | 6 | from datetime import datetime 7 | import locale 8 | import socket 9 | from nwg_hello.tools import * 10 | from nwg_hello.ui import * 11 | from nwg_hello.__about__ import __version__ 12 | 13 | gi.require_version("Gtk", "3.0") 14 | gi.require_version("Gdk", "3.0") 15 | 16 | try: 17 | gi.require_version('GtkLayerShell', '0.1') 18 | except ValueError: 19 | 20 | raise RuntimeError('\n\n' + 21 | 'If you haven\'t installed GTK Layer Shell, you need to point Python to the\n' + 22 | 'library by setting GI_TYPELIB_PATH and LD_LIBRARY_PATH to /src/.\n' + 23 | 'For example you might need to run:\n\n' + 24 | 'GI_TYPELIB_PATH=build/src LD_LIBRARY_PATH=build/src python3 ' + ' '.join(sys.argv)) 25 | 26 | from gi.repository import GLib, GtkLayerShell, Gtk, Gdk 27 | 28 | dir_name = os.path.dirname(__file__) 29 | 30 | log_file = os.path.join(temp_dir(), 'nwg-hello.log') 31 | voc = {} 32 | windows = [] 33 | 34 | parser = argparse.ArgumentParser() 35 | parser.add_argument("-d", "--debug", action="store_true", help="print Debug messages to stderr") 36 | parser.add_argument("-l", "--log", action="store_true", help=f"save output to '{log_file}' file") 37 | parser.add_argument("-t", "--test", action="store_true", help="Test GUI w/o connecting to daemon") 38 | parser.add_argument("-v", "--version", action="version", version="%(prog)s version {}".format(__version__), 39 | help="display Version information") 40 | 41 | args = parser.parse_args() 42 | 43 | if args.log: 44 | args.debug = True 45 | 46 | if args.log and os.getenv("USER") == "greeter": 47 | now = datetime.now() 48 | eprint(f'[nwg-hello log {now.strftime("%Y-%m-%d %H:%M:%S")}]', log=True) 49 | 50 | # Load settings 51 | settings_path = "/etc/nwg-hello/nwg-hello.json" if os.path.isfile( 52 | "/etc/nwg-hello/nwg-hello.json") else "/etc/nwg-hello/nwg-hello-default.json" 53 | settings = load_json(settings_path) 54 | if settings and args.debug: 55 | eprint(f"Loaded settings from: '{settings_path}'", log=args.log) 56 | # set defaults if key not found 57 | defaults = { 58 | "session_dirs": [ 59 | "/usr/share/wayland-sessions", 60 | "/usr/share/xsessions" 61 | ], 62 | "custom_sessions": [ 63 | { 64 | "name": "Shell", 65 | "exec": "/usr/bin/bash" 66 | } 67 | ], 68 | "monitor_nums": [], 69 | "form_on_monitors": [], 70 | "delay_secs": 1, 71 | "cmd-sleep": "systemctl suspend", 72 | "cmd-reboot": "systemctl reboot", 73 | "cmd-poweroff": "systemctl poweroff", 74 | "gtk-theme": "Adwaita", 75 | "gtk-icon-theme": "", 76 | "gtk-cursor-theme": "", 77 | "prefer-dark-theme": True, 78 | "template-name": "", 79 | "time-format": "%H:%M:%S", 80 | "date-format": "%A, %d. %B", 81 | "layer": "overlay", 82 | "keyboard-mode": "exclusive", 83 | "lang": "", 84 | "avatar-show": False, 85 | "avatar-size": 100, 86 | "avatar-border-width": 1, 87 | "avatar-border-color": "#eee", 88 | "avatar-corner-radius": 20, 89 | "avatar-circle": False, 90 | "env-vars": [] 91 | } 92 | for key in defaults: 93 | if key not in settings: 94 | eprint(f"Settings: using default value for '{key}'", log=args.log) 95 | settings[key] = defaults[key] 96 | 97 | # load cache (the file has been preinstalled and belongs to the 'greeter' user) 98 | cache = load_json("/var/cache/nwg-hello/cache.json") 99 | if args.debug: 100 | eprint(f"Loaded cache: {cache}", log=args.log) 101 | 102 | if args.debug: 103 | eprint(f"Config session_dirs: {settings['session_dirs']}", log=args.log) 104 | if settings["custom_sessions"]: 105 | eprint(f"Config custom_sessions: {settings['custom_sessions']}", log=args.log) 106 | if settings['lang']: 107 | eprint(f"Config lang: {settings['lang']}", log=args.log) 108 | 109 | if not args.test: 110 | eprint("Attempting to connect the client:", log=args.log) 111 | try: 112 | g_socket = os.getenv("GREETD_SOCK") 113 | eprint(f"socket = '{g_socket}'", log=args.log) 114 | client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 115 | eprint(f"client = '{client}'", log=args.log) 116 | client.connect(g_socket) 117 | except Exception as e: 118 | eprint(f"Could not connect: {e}", log=args.log) 119 | args.test = True 120 | else: 121 | eprint(f"Testing, skipped client connection", log=args.log) 122 | client = None 123 | 124 | # Load vocabulary 125 | if os.path.isfile("/etc/nwg-hello/en_US"): 126 | # allow user-defined basic lang file in /etc/nwg-hello #19 127 | voc = load_json("/etc/nwg-hello/en_US") 128 | if not voc: 129 | # couldn't load json! 130 | eprint(f"Could not load /etc/nwg-hello/en_US, loading default file instead.") 131 | voc = load_json(os.path.join(dir_name, "langs", "en_US")) 132 | else: 133 | if args.debug: 134 | eprint(f"Using /etc/nwg-hello/en_US lang file") 135 | else: 136 | # load predefined basic lang file 137 | voc = load_json(os.path.join(dir_name, "langs", "en_US")) 138 | 139 | user_locale = locale.getlocale()[0] if not settings["lang"] else settings["lang"] 140 | # translate if necessary (and if we have a translation) 141 | if user_locale != "en_US" and user_locale in os.listdir(os.path.join(dir_name, "langs")): 142 | # translated phrases 143 | if os.path.isfile(os.path.join("/etc/nwg-hello", user_locale)): 144 | # allow user-defined lang files in /etc/nwg-hello #19 145 | loc = load_json(os.path.join("/etc/nwg-hello", user_locale)) 146 | if not loc: 147 | # couldn't load json! 148 | eprint(f"Could not load {os.path.join('/etc/nwg-hello', user_locale)}, loading default file instead.") 149 | loc = load_json(os.path.join(dir_name, "langs", user_locale)) 150 | else: 151 | if args.debug: 152 | eprint(f"Using /etc/nwg-hello/{user_locale} lang file") 153 | else: 154 | # load predefined lang file 155 | loc = load_json(os.path.join(dir_name, "langs", user_locale)) 156 | for key in voc: 157 | if key in loc: 158 | voc[key] = loc[key] 159 | if args.debug: 160 | eprint(f"Vocabulary translated into: '{user_locale}'", log=args.log) 161 | 162 | # List users 163 | users = list_users(log=args.log) 164 | if args.debug: 165 | eprint(f"Found users: {users}", log=args.log) 166 | 167 | # List sessions 168 | sessions, x_sessions = list_sessions(settings['session_dirs']) 169 | if args.debug: 170 | eprint(f"Found valid sessions: {sessions}", log=args.log) 171 | eprint(f"X11 sessions: {x_sessions}", log=args.log) 172 | 173 | 174 | def set_clock(): 175 | _now = datetime.now() 176 | for win in windows: 177 | win.update_time(_now, settings["time-format"], settings["date-format"]) 178 | return False 179 | 180 | 181 | def move_clock(): 182 | _now = datetime.now() 183 | for win in windows: 184 | win.update_time(_now, settings["time-format"], settings["date-format"]) 185 | return True 186 | 187 | 188 | def emulate_mouse_event(): 189 | # To focus the window -> password form entry, we need to perform some mouse event. 190 | # Although I tried hard, nothing worked well on Hyprland 0.43.0, so we'll only do it for sway. 191 | if os.getenv('SWAYSOCK'): 192 | subprocess.Popen("swaymsg seat - cursor release button1", shell=True) 193 | 194 | return False 195 | 196 | 197 | def main(): 198 | # Some monitors take longer to startup; we can just time.sleep() here, as we're not yet on the GTK loop. 199 | if settings["delay_secs"] > 0: 200 | time.sleep(settings["delay_secs"]) 201 | 202 | # Load css 203 | screen = Gdk.Screen.get_default() 204 | provider = Gtk.CssProvider() 205 | style_context = Gtk.StyleContext() 206 | style_context.add_provider_for_screen(screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) 207 | try: 208 | style_path = "/etc/nwg-hello/nwg-hello.css" if os.path.isfile( 209 | "/etc/nwg-hello/nwg-hello.css") else "/etc/nwg-hello/nwg-hello-default.css" 210 | provider.load_from_path(style_path) 211 | if args.debug: 212 | eprint(f"Loaded style from: '{style_path}'", log=args.log) 213 | except Exception as e: 214 | eprint(f"* {e}", log=args.log) 215 | 216 | gtk_settings = Gtk.Settings.get_default() 217 | gtk_settings.set_property("gtk-application-prefer-dark-theme", True) 218 | if settings["gtk-theme"]: 219 | gtk_settings.set_property("gtk-theme-name", settings["gtk-theme"]) 220 | if settings["gtk-icon-theme"]: 221 | gtk_settings.set_property("gtk-icon-theme-name", settings["gtk-icon-theme"]) 222 | if settings["gtk-cursor-theme"]: 223 | gtk_settings.set_property("gtk-cursor-theme-name", settings["gtk-cursor-theme"]) 224 | 225 | # Create UI for selected or all monitors 226 | global windows 227 | display = Gdk.Display.get_default() 228 | for i in reversed(range(display.get_n_monitors())): 229 | if not settings["monitor_nums"] or i in settings["monitor_nums"]: 230 | monitor = display.get_monitor(i) 231 | if not settings["form_on_monitors"] or i in settings["form_on_monitors"]: 232 | win = GreeterWindow(client, settings, sessions, x_sessions, users, monitor, voc, cache, args.log, 233 | args.test) 234 | windows.append(win) 235 | else: 236 | win = EmptyWindow(settings, monitor, args.log, args.test) 237 | 238 | GLib.timeout_add(0, set_clock) 239 | GLib.timeout_add(500, move_clock) 240 | 241 | GLib.timeout_add(1000, emulate_mouse_event) 242 | 243 | Gtk.main() 244 | 245 | 246 | if __name__ == "__main__": 247 | sys.exit(main()) 248 | -------------------------------------------------------------------------------- /nwg_hello/template.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | False 7 | 8 | 9 | True 10 | False 11 | 12 | 13 | True 14 | False 15 | vertical 16 | 17 | 18 | True 19 | False 20 | vertical 21 | 22 | 23 | 400 24 | True 25 | False 26 | center 27 | 60 28 | 60 29 | True 30 | vertical 31 | 32 | 33 | True 34 | False 35 | 12 36 | vertical 37 | 38 | 39 | True 40 | False 41 | 20 42 | Welcome! 43 | 44 | 45 | False 46 | True 47 | 0 48 | 49 | 50 | 51 | 52 | True 53 | False 54 | 12 55 | 00:00 56 | 57 | 58 | False 59 | True 60 | 1 61 | 62 | 63 | 64 | 65 | True 66 | False 67 | Monday, 13. January 68 | 69 | 70 | False 71 | True 72 | 2 73 | 74 | 75 | 76 | 77 | False 78 | True 79 | 0 80 | 81 | 82 | 83 | 84 | True 85 | False 86 | 24 87 | vertical 88 | 6 89 | 90 | 91 | True 92 | False 93 | 94 | 95 | 96 | 97 | 98 | False 99 | True 100 | 0 101 | 102 | 103 | 104 | 105 | True 106 | False 107 | start 108 | Session: 109 | 110 | 111 | False 112 | True 113 | 1 114 | 115 | 116 | 117 | 118 | True 119 | False 120 | 121 | 122 | False 123 | True 124 | 2 125 | 126 | 127 | 128 | 129 | True 130 | False 131 | start 132 | 12 133 | User: 134 | 135 | 136 | False 137 | True 138 | 3 139 | 140 | 141 | 142 | 143 | True 144 | False 145 | 146 | 147 | False 148 | True 149 | 4 150 | 151 | 152 | 153 | 154 | True 155 | False 156 | start 157 | 12 158 | Password: 159 | 160 | 161 | False 162 | True 163 | 5 164 | 165 | 166 | 167 | 168 | True 169 | True 170 | 171 | 172 | False 173 | True 174 | 6 175 | 176 | 177 | 178 | 179 | Show password 180 | True 181 | True 182 | False 183 | True 184 | 185 | 186 | False 187 | True 188 | 7 189 | 190 | 191 | 192 | 193 | False 194 | True 195 | 1 196 | 197 | 198 | 199 | 200 | True 201 | False 202 | 12 203 | vertical 204 | 205 | 206 | True 207 | False 208 | 24 209 | 12 210 | Some message 211 | 212 | 213 | False 214 | True 215 | 0 216 | 217 | 218 | 219 | 220 | Login 221 | True 222 | True 223 | True 224 | 12 225 | 226 | 227 | False 228 | True 229 | 1 230 | 231 | 232 | 233 | 234 | True 235 | False 236 | 24 237 | 238 | 239 | True 240 | False 241 | 12 242 | True 243 | 244 | 245 | Sleep 246 | True 247 | True 248 | True 249 | 250 | 251 | False 252 | True 253 | 0 254 | 255 | 256 | 257 | 258 | Restart 259 | True 260 | True 261 | True 262 | 263 | 264 | False 265 | True 266 | 1 267 | 268 | 269 | 270 | 271 | Poweroff 272 | True 273 | True 274 | True 275 | 276 | 277 | False 278 | True 279 | 2 280 | 281 | 282 | 283 | 284 | True 285 | False 286 | 0 287 | 288 | 289 | 290 | 291 | False 292 | True 293 | 2 294 | 295 | 296 | 297 | 298 | False 299 | True 300 | 2 301 | 302 | 303 | 304 | 305 | False 306 | True 307 | 0 308 | 309 | 310 | 311 | 312 | True 313 | False 314 | 0 315 | 316 | 317 | 318 | 319 | False 320 | False 321 | 0 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | -------------------------------------------------------------------------------- /nwg_hello/tools.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import subprocess 4 | import sys 5 | 6 | 7 | def temp_dir(): 8 | if os.getenv("TMPDIR"): 9 | return os.getenv("TMPDIR") 10 | elif os.getenv("TEMP"): 11 | return os.getenv("TEMP") 12 | elif os.getenv("TMP"): 13 | return os.getenv("TMP") 14 | return "/tmp" 15 | 16 | 17 | def eprint(*args, log=False): 18 | print(*args, file=sys.stderr) 19 | # we don't log testing sessions 20 | if log and os.getenv("USER") == "greeter": 21 | log_file = os.path.join(temp_dir(), 'nwg-hello.log') 22 | with open(log_file, 'a') as f: 23 | print(*args, file=f) 24 | 25 | 26 | def list_users(log=False): 27 | uid_min = 1000 28 | try: 29 | with open('/etc/login.defs') as log_list: 30 | for line in log_list.readlines(): 31 | if line.startswith('UID_MIN'): 32 | uid_min = int(line.split(' ')[1]) 33 | except Exception as e: 34 | eprint(f"Couldn't get min uid: '{e}'", log=log) 35 | 36 | users = [] 37 | for uname in os.listdir('/home'): 38 | try: 39 | # ask pam about users 40 | user = subprocess.check_output(['getent', 'passwd', uname]).decode('utf-8').strip() 41 | except Exception as e: 42 | # skip nonexistent users 43 | eprint(e) 44 | continue 45 | if user: 46 | user = user.split(':') 47 | if int(user[2]) >= uid_min: 48 | users.append(user[0]) 49 | return users 50 | 51 | 52 | def load_json(path): 53 | try: 54 | with open(path, 'r') as f: 55 | return json.load(f) 56 | except Exception as e: 57 | eprint(f"* error loading json: {e}") 58 | return {} 59 | 60 | 61 | def save_json(src_dict, path, en_ascii=True, log=False): 62 | try: 63 | with open(path, 'w') as f: 64 | json.dump(src_dict, f, indent=2, ensure_ascii=en_ascii) 65 | eprint(f"Saved json to: '{src_dict}'", log=log) 66 | except Exception as e: 67 | eprint(f"Error saving json: '{f}'", log=log) 68 | 69 | 70 | def load_text_file(path): 71 | try: 72 | with open(path, 'r') as file: 73 | data = file.read() 74 | return data 75 | except Exception as e: 76 | eprint(e) 77 | return None 78 | 79 | 80 | def list_sessions(session_dirs): 81 | _sessions = [] 82 | _x_sessions = [] 83 | for session_dir in session_dirs: 84 | if os.path.isdir(session_dir): 85 | for file_name in sorted(os.listdir(session_dir)): 86 | p = os.path.join(session_dir, file_name) 87 | if p.endswith('.desktop'): 88 | session = parse_desktop_entry(p) 89 | if session: 90 | _sessions.append(session) 91 | if session_dir == "/usr/share/xsessions": 92 | _x_sessions.append(session["exec"]) 93 | return _sessions, _x_sessions 94 | 95 | 96 | def launch(self, cmd, log=False): 97 | eprint("Executing cmd: '{}'".format(cmd), log=log) 98 | # Alright, I know. But subprocess sucks with systemd. 99 | eprint(os.popen(cmd).read(), log=log) 100 | 101 | 102 | def parse_desktop_entry(path): 103 | paths = os.getenv('PATH').split(":") 104 | session = {} 105 | lines = load_text_file(path).splitlines() 106 | if lines: 107 | for line in lines: 108 | if line.startswith('Name'): 109 | session['name'] = line.split("=")[1] 110 | continue 111 | if line.startswith('Exec'): 112 | session['exec'] = line.split("=")[1] 113 | continue 114 | if line.startswith('TryExec'): 115 | session['try-exec'] = line.split("=")[1] 116 | continue 117 | if 'try-exec' in session: 118 | if os.path.isfile(session['try-exec']): 119 | return session 120 | else: 121 | for p in paths: 122 | if os.path.isfile(os.path.join(p, session['try-exec'])): 123 | return session 124 | else: 125 | return session 126 | else: 127 | return None 128 | 129 | 130 | def greetd(client, json_req, log=False): 131 | eprint(f"greetd: request = {json_req}", log=log) 132 | req = json.dumps(json_req) 133 | client.send(len(req).to_bytes(4, "little") + req.encode("utf-8")) 134 | resp_raw = client.recv(128) 135 | resp_len = int.from_bytes(resp_raw[0:4], "little") 136 | resp_trimmed = resp_raw[4:resp_len + 4].decode() 137 | try: 138 | r = json.loads(resp_trimmed) 139 | eprint(f"greetd: response = {r}", log=log) 140 | return r 141 | except ValueError: 142 | eprint(f"greetd: ValueError") 143 | return {} 144 | -------------------------------------------------------------------------------- /nwg_hello/ui.py: -------------------------------------------------------------------------------- 1 | import os 2 | import gi 3 | import sys 4 | 5 | gi.require_version('Gtk', '3.0') 6 | gi.require_version('GtkLayerShell', '0.1') 7 | from gi.repository import Gtk, Gdk, GtkLayerShell, GdkPixbuf 8 | from nwg_hello.tools import eprint, greetd, launch, save_json 9 | 10 | 11 | def p_icon_path(icon_name): 12 | # allow user-defined icons 13 | if os.path.isfile(f"/usr/share/nwg-hello/{icon_name}.svg"): 14 | return f"/usr/share/nwg-hello/{icon_name}.svg" 15 | else: 16 | return f"/usr/share/nwg-hello/{icon_name}-default.svg" 17 | 18 | 19 | class GreeterWindow(Gtk.Window): 20 | def __init__(self, client, settings, sessions, x_sessions, users, monitor, voc, cache, log, test): 21 | eprint(f"Creating GreeterWindow on {monitor}", log=log) 22 | 23 | self.settings = settings 24 | self.voc = voc 25 | self.log = log 26 | self.client = client 27 | self.sessions = sessions 28 | self.x_sessions = x_sessions # contains session execs, not names 29 | self.test = test 30 | self.cache = cache # store cache with all user sessions for later 31 | 32 | dir_name = os.path.dirname(__file__) 33 | 34 | Gtk.Window.__init__(self) 35 | 36 | builder = Gtk.Builder() 37 | if self.settings["template-name"] and os.path.isfile( 38 | os.path.join("/etc/nwg-hello", self.settings["template-name"])): 39 | # use custom template if name configured (must be placed in /etc/nwg-hello/) 40 | builder.add_from_file( 41 | os.path.join(os.path.join("/etc/nwg-hello", self.settings["template-name"]))) 42 | else: 43 | # use built-in template 44 | # (/usr/lib/python3.xx/site-packages/nwg_hello-x.y.z-py3.xx.egg/nwg_hello/template.glade) 45 | builder.add_from_file(os.path.join(dir_name, "template.glade")) 46 | 47 | form_wrapper = builder.get_object("form-wrapper") 48 | form_wrapper.set_property("name", "form-wrapper") 49 | 50 | lbl_welcome = builder.get_object("lbl-welcome") 51 | lbl_welcome.set_text(f'{voc["welcome"]}') 52 | lbl_welcome.set_property("name", "welcome-label") 53 | 54 | self.lbl_clock = builder.get_object("lbl-clock") 55 | self.lbl_clock.set_property("name", "clock-label") 56 | 57 | self.lbl_date = builder.get_object("lbl-date") 58 | self.lbl_date.set_property("name", "date-label") 59 | 60 | if self.settings["avatar-show"]: 61 | self.avatar_wrapper = builder.get_object("avatar-wrapper") 62 | 63 | lbl_session = builder.get_object("lbl-session") 64 | lbl_session.set_property("name", "form-label") 65 | lbl_session.set_text(f'{voc["session"]}:') 66 | 67 | self.combo_session = builder.get_object("combo-session") 68 | self.combo_session.set_property("name", "form-combo") 69 | for session in sessions: 70 | self.combo_session.append(session["exec"], session["name"]) 71 | if settings["custom_sessions"]: 72 | for item in settings["custom_sessions"]: 73 | self.combo_session.append(item["exec"], item["name"]) 74 | if ("user" and "sessions") in self.cache and \ 75 | self.cache["user"] in self.cache["sessions"]: 76 | # preselect the session stored in cache for the last user 77 | self.combo_session.set_active_id(self.cache["sessions"][self.cache["user"]]) 78 | else: 79 | self.combo_session.set_active_id(sessions[0]["name"]) 80 | self.combo_session.connect("changed", self.on_session_changed) 81 | 82 | lbl_user = builder.get_object("lbl-user") 83 | lbl_user.set_property("name", "form-label") 84 | lbl_user.set_text(f'{voc["user"]}:') 85 | 86 | lbl_password = builder.get_object("lbl-password") 87 | lbl_password.set_property("name", "form-label") 88 | lbl_password.set_text(f'{voc["password"]}:') 89 | 90 | self.entry_password = builder.get_object("entry-password") 91 | self.entry_password.set_property("name", "password-entry") 92 | self.entry_password.set_visibility(False) 93 | self.entry_password.connect("button-press-event", self.clear_message_label) 94 | 95 | cb_show_password = builder.get_object("cb-show-password") 96 | cb_show_password.set_label(voc["show-password"]) 97 | cb_show_password.connect("toggled", self.on_password_cb) 98 | 99 | self.lbl_message = builder.get_object("lbl-message") 100 | self.lbl_message.set_text("") 101 | 102 | self.combo_user = builder.get_object("combo-user") 103 | self.combo_user.set_property("name", "form-combo") 104 | for user in users: 105 | self.combo_user.append(user, user) 106 | self.combo_user.connect("changed", self.on_user_changed) 107 | if "user" in self.cache and self.cache["user"]: 108 | # preselect the user stored in the cache 109 | self.combo_user.set_active_id(self.cache["user"]) 110 | else: 111 | # or the 1st user 112 | self.combo_user.set_active_id(users[0]) 113 | 114 | # password and message label moved up, as we've just connected user combo to on_user_changed(), 115 | # that needs them to be already declared 116 | 117 | btn_login = builder.get_object("btn-login") 118 | btn_login.set_property("name", "login-button") 119 | btn_login.set_label(voc["login"]) 120 | btn_login.connect("clicked", self.login) 121 | 122 | btn_sleep = builder.get_object("btn-sleep") 123 | btn_sleep.set_property("name", "power-button") 124 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(os.path.join(p_icon_path("sleep")), 48, 48) 125 | img = Gtk.Image.new_from_pixbuf(pixbuf) 126 | btn_sleep.set_image(img) 127 | btn_sleep.set_always_show_image(True) 128 | btn_sleep.set_image_position(Gtk.PositionType.TOP) 129 | btn_sleep.set_label(voc["sleep"]) 130 | btn_sleep.connect("clicked", launch, settings["cmd-sleep"], self.log) 131 | 132 | btn_restart = builder.get_object("btn-restart") 133 | btn_restart.set_property("name", "power-button") 134 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(os.path.join(p_icon_path("reboot")), 48, 48) 135 | img = Gtk.Image.new_from_pixbuf(pixbuf) 136 | btn_restart.set_image(img) 137 | btn_restart.set_always_show_image(True) 138 | btn_restart.set_image_position(Gtk.PositionType.TOP) 139 | btn_restart.set_label(voc["reboot"]) 140 | btn_restart.connect("clicked", launch, settings["cmd-reboot"], self.log) 141 | 142 | btn_poweroff = builder.get_object("btn-poweroff") 143 | btn_poweroff.set_property("name", "power-button") 144 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(os.path.join(p_icon_path("poweroff")), 48, 48) 145 | img = Gtk.Image.new_from_pixbuf(pixbuf) 146 | btn_poweroff.set_image(img) 147 | btn_poweroff.set_always_show_image(True) 148 | btn_poweroff.set_image_position(Gtk.PositionType.TOP) 149 | btn_poweroff.set_label(voc["power-off"]) 150 | btn_poweroff.connect("clicked", launch, settings["cmd-poweroff"], self.log) 151 | 152 | self.window = builder.get_object("main-window") 153 | self.window.connect('destroy', Gtk.main_quit) 154 | self.window.connect("key-release-event", self.handle_keyboard) 155 | 156 | GtkLayerShell.init_for_window(self.window) 157 | GtkLayerShell.set_monitor(self.window, monitor) 158 | 159 | if self.settings["layer"].upper() == "BACKGROUND": 160 | GtkLayerShell.set_layer(self.window, GtkLayerShell.Layer.BACKGROUND) 161 | elif self.settings["layer"].upper() == "BOTTOM": 162 | GtkLayerShell.set_layer(self.window, GtkLayerShell.Layer.BOTTOM) 163 | elif self.settings["layer"].upper() == "TOP": 164 | GtkLayerShell.set_layer(self.window, GtkLayerShell.Layer.TOP) 165 | else: 166 | GtkLayerShell.set_layer(self.window, GtkLayerShell.Layer.OVERLAY) 167 | 168 | if self.settings["keyboard-mode"].upper() == "NONE": 169 | GtkLayerShell.set_keyboard_mode(self.window, GtkLayerShell.KeyboardMode.NONE) 170 | elif self.settings["keyboard-mode"].upper() == "ON_DEMAND" or self.settings[ 171 | "keyboard-mode"].upper() == "ON-DEMAND": 172 | GtkLayerShell.set_keyboard_mode(self.window, GtkLayerShell.KeyboardMode.ON_DEMAND) 173 | else: 174 | GtkLayerShell.set_keyboard_mode(self.window, GtkLayerShell.KeyboardMode.EXCLUSIVE) 175 | 176 | GtkLayerShell.set_anchor(self.window, GtkLayerShell.Edge.TOP, True) 177 | GtkLayerShell.set_anchor(self.window, GtkLayerShell.Edge.BOTTOM, True) 178 | GtkLayerShell.set_anchor(self.window, GtkLayerShell.Edge.LEFT, True) 179 | GtkLayerShell.set_anchor(self.window, GtkLayerShell.Edge.RIGHT, True) 180 | GtkLayerShell.set_exclusive_zone(self.window, -1) 181 | GtkLayerShell.set_namespace(self.window, "nwg-hello") 182 | 183 | self.window.show() 184 | 185 | form_wrapper.set_size_request(monitor.get_geometry().width * 0.37, 0) 186 | self.entry_password.grab_focus() 187 | 188 | def handle_keyboard(self, w, event): 189 | if event.type == Gdk.EventType.KEY_RELEASE: 190 | if self.test and event.keyval == Gdk.KEY_Escape: 191 | Gtk.main_quit() 192 | elif event.keyval == Gdk.KEY_Return: 193 | self.login(None) 194 | 195 | return True 196 | 197 | def update_time(self, now, time_format, date_format): 198 | self.lbl_clock.set_text(f'{now.strftime(time_format)}') 199 | self.lbl_date.set_text(f'{now.strftime(date_format)}') 200 | 201 | def on_session_changed(self, combo): 202 | self.entry_password.grab_focus() 203 | self.clear_message_label() 204 | 205 | def on_user_changed(self, combo): 206 | selected_user = self.combo_user.get_active_id() 207 | if self.settings["avatar-show"]: 208 | # Look up user avatar 209 | paths = [ 210 | f"/var/lib/AccountsService/icons/{selected_user}", 211 | f"/var/lib/avatars/{selected_user}/.face", 212 | os.path.join(p_icon_path("avatar")) 213 | ] 214 | for p in paths: 215 | if os.path.exists(p): 216 | for c in self.avatar_wrapper.get_children(): 217 | c.destroy() 218 | img = RoundedImage(os.path.join(p), self.settings["avatar-size"], 219 | self.settings["avatar-border-width"], self.settings["avatar-border-color"], 220 | self.settings["avatar-circle"], self.settings["avatar-corner-radius"]) 221 | self.avatar_wrapper.pack_start(img, True, False, 0) 222 | self.avatar_wrapper.show_all() 223 | break 224 | 225 | if "sessions" in self.cache and selected_user in self.cache["sessions"]: 226 | # preselect user session if available in cache 227 | self.combo_session.set_active_id(self.cache["sessions"][selected_user]) 228 | self.entry_password.grab_focus() 229 | self.clear_message_label() 230 | 231 | def clear_message_label(self, *args): 232 | self.lbl_message.set_text("") 233 | 234 | def on_password_cb(self, widget): 235 | self.entry_password.set_visibility(widget.get_active()) 236 | 237 | def login(self, btn): 238 | # https://github.com/nwg-piotr/nwg-hello/issues/34 239 | # if not self.entry_password.get_text(): 240 | # eprint("Login: passwd empty, cancelling", log=self.log) 241 | # self.lbl_message.set_text(self.voc["password-empty"]) 242 | # return 243 | if self.client: 244 | try: 245 | jreq = {"type": "cancel_session"} 246 | resp = greetd(self.client, jreq, log=self.log) 247 | except: 248 | pass 249 | 250 | user = self.combo_user.get_active_id() 251 | password = self.entry_password.get_text() 252 | cmd = self.combo_session.get_active_id() 253 | eprint(f"user: {user}", log=self.log) 254 | eprint(f"password: {'*' * len(password)}", log=self.log) 255 | eprint(f"cmd: {cmd}", log=self.log) 256 | eprint(f"env vars: {self.settings['env-vars']}", log=self.log) 257 | 258 | jreq = {"type": "create_session", "username": user} 259 | try: 260 | resp = greetd(self.client, jreq, log=self.log) 261 | except Exception as e: 262 | eprint(e, log=self.log) 263 | 264 | jreq = {"type": "post_auth_message_response", "response": password} 265 | resp = greetd(self.client, jreq, log=self.log) 266 | if "error_type" in resp and resp["error_type"] == "auth_error": 267 | self.lbl_message.set_text(self.voc["login-failed"]) 268 | self.entry_password.set_text("") 269 | else: 270 | # ensure the sessions dict exists in cache 271 | if "sessions" not in self.cache: 272 | self.cache["sessions"] = {} 273 | 274 | # store last used session name and username if both available 275 | if self.combo_user.get_active_id(): 276 | self.cache["user"] = self.combo_user.get_active_id() 277 | if self.combo_session.get_active_id(): 278 | self.cache["sessions"][self.cache["user"]] = self.combo_session.get_active_id() 279 | if self.cache["user"] and self.cache["sessions"][self.cache["user"]]: 280 | eprint(f"Saving cache: {self.cache}", log=self.log) 281 | # this file belongs to the 'greeter' user 282 | try: 283 | save_json(self.cache, "/var/cache/nwg-hello/cache.json", log=self.log) 284 | eprint("Cache saved", log=self.log) 285 | except Exception as e: 286 | eprint(f"Error saving cache: {e}", log=self.log) 287 | 288 | if cmd in self.x_sessions: 289 | jreq = {"type": "start_session", "cmd": ["startx", "/usr/bin/env"] + cmd.split(), 290 | "env": self.settings["env-vars"]} 291 | else: 292 | jreq = {"type": "start_session", "cmd": cmd.split(), "env": self.settings["env-vars"]} 293 | 294 | resp = greetd(self.client, jreq, log=self.log) 295 | if "type" in resp and resp["type"] == "success": 296 | sys.exit() 297 | 298 | 299 | class EmptyWindow(Gtk.Window): 300 | def __init__(self, settings, monitor, log, test): 301 | eprint(f"Creating EmptyWindow on {monitor}", log=log) 302 | 303 | self.test = test 304 | 305 | Gtk.Window.__init__(self) 306 | 307 | self.connect('destroy', Gtk.main_quit) 308 | self.connect("key-release-event", self.handle_keyboard) 309 | 310 | GtkLayerShell.init_for_window(self) 311 | GtkLayerShell.set_monitor(self, monitor) 312 | 313 | if settings["layer"].upper() == "BACKGROUND": 314 | GtkLayerShell.set_layer(self, GtkLayerShell.Layer.BACKGROUND) 315 | elif settings["layer"].upper() == "BOTTOM": 316 | GtkLayerShell.set_layer(self, GtkLayerShell.Layer.BOTTOM) 317 | elif settings["layer"].upper() == "TOP": 318 | GtkLayerShell.set_layer(self, GtkLayerShell.Layer.TOP) 319 | else: 320 | GtkLayerShell.set_layer(self, GtkLayerShell.Layer.OVERLAY) 321 | 322 | if settings["keyboard-mode"].upper() == "NONE": 323 | GtkLayerShell.set_keyboard_mode(self, GtkLayerShell.KeyboardMode.NONE) 324 | elif settings["keyboard-mode"].upper() == "ON_DEMAND": 325 | GtkLayerShell.set_keyboard_mode(self, GtkLayerShell.KeyboardMode.ON_DEMAND) 326 | else: 327 | GtkLayerShell.set_keyboard_mode(self, GtkLayerShell.KeyboardMode.EXCLUSIVE) 328 | 329 | GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.TOP, True) 330 | GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.BOTTOM, True) 331 | GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.LEFT, True) 332 | GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.RIGHT, True) 333 | GtkLayerShell.set_exclusive_zone(self, -1) 334 | GtkLayerShell.set_namespace(self, "nwg-hello") 335 | 336 | self.show() 337 | 338 | def handle_keyboard(self, w, event): 339 | if event.type == Gdk.EventType.KEY_RELEASE: 340 | if self.test and event.keyval == Gdk.KEY_Escape: 341 | Gtk.main_quit() 342 | 343 | return True 344 | 345 | 346 | def hex_to_rgb(hex_color): 347 | hex_color = hex_color.lstrip('#') 348 | if len(hex_color) == 6: 349 | r, g, b = [int(hex_color[i:i + 2], 16) / 255.0 for i in (0, 2, 4)] 350 | elif len(hex_color) == 3: 351 | r, g, b = [int(c * 2, 16) / 255.0 for c in hex_color] 352 | else: 353 | raise ValueError("Invalid hex color format. Use #rrggbb or #rgb.") 354 | 355 | return r, g, b 356 | 357 | 358 | class RoundedImage(Gtk.DrawingArea): 359 | def __init__(self, image_path, size=100, border_width=1, border_color="#eee", circle=False, corner_radius=15): 360 | super().__init__() 361 | self.image_path = image_path 362 | self.size = size 363 | self.border_width = border_width 364 | self.circle = circle # circle or rounded square 365 | self.corner_radius = corner_radius 366 | try: 367 | self.border_color = hex_to_rgb(border_color) 368 | except ValueError as e: 369 | eprint(e) 370 | self.border_color = (0, 0, 0) 371 | 372 | self.connect("draw", self.on_draw) 373 | self.set_size_request(size, size) 374 | 375 | def draw_rounded_rectangle(self, cr, x, y, width, height, radius): 376 | cr.new_sub_path() 377 | cr.arc(x + width - radius, y + radius, radius, -0.5 * 3.14, 0) 378 | cr.arc(x + width - radius, y + height - radius, radius, 0, 0.5 * 3.14) 379 | cr.arc(x + radius, y + height - radius, radius, 0.5 * 3.14, 3.14) 380 | cr.arc(x + radius, y + radius, radius, 3.14, 1.5 * 3.14) 381 | cr.close_path() 382 | 383 | def on_draw(self, widget, cr): 384 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(self.image_path, self.size, self.size) 385 | Gdk.cairo_set_source_pixbuf(cr, pixbuf, 0, 0) 386 | 387 | if self.circle: 388 | cr.arc(self.size / 2, self.size / 2, self.size / 2, 0, 2 * 3.14) 389 | else: 390 | self.draw_rounded_rectangle(cr, 0, 0, self.size, self.size, self.corner_radius) 391 | 392 | cr.clip() 393 | cr.paint() 394 | 395 | cr.reset_clip() 396 | cr.set_source_rgb(*self.border_color) 397 | cr.set_line_width(self.border_width) 398 | 399 | if self.circle: 400 | radius = self.size / 2 - self.border_width / 2 401 | cr.arc(self.size / 2, self.size / 2, radius, 0, 2 * 3.14) 402 | else: 403 | self.draw_rounded_rectangle(cr, self.border_width / 2, self.border_width / 2, 404 | self.size - self.border_width, self.size - self.border_width, 405 | self.corner_radius) 406 | cr.stroke() 407 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup, find_packages 4 | 5 | 6 | def read(f_name): 7 | return open(os.path.join(os.path.dirname(__file__), f_name)).read() 8 | 9 | 10 | setup( 11 | name='nwg-hello', 12 | version='0.4.0', 13 | description='GTK3-based greeter for greetd', 14 | packages=find_packages(), 15 | include_package_data=True, 16 | package_data={ 17 | "": ["img/*", "langs/*", "template.glade"] 18 | }, 19 | url='https://github.com/nwg-piotr/nwg-greeter', 20 | license='MIT', 21 | author='Piotr Miller', 22 | author_email='nwg.piotr@gmail.com', 23 | python_requires='>=3.6.0', 24 | install_requires=[], 25 | entry_points={ 26 | 'gui_scripts': [ 27 | 'nwg-hello = nwg_hello.main:main', 28 | ] 29 | } 30 | ) 31 | -------------------------------------------------------------------------------- /sway-config: -------------------------------------------------------------------------------- 1 | exec "nwg-hello; swaymsg exit" 2 | 3 | bindsym Mod4+shift+e exec swaynag \ 4 | -t warning \ 5 | -m 'What do you want to do?' \ 6 | -b 'Poweroff' 'systemctl poweroff' \ 7 | -b 'Reboot' 'systemctl reboot' 8 | 9 | include /etc/sway/config.d/* -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PROGRAM_NAME="nwg-hello" 4 | MODULE_NAME="nwg_hello" 5 | SITE_PACKAGES="$(python3 -c "import sysconfig; print(sysconfig.get_paths()['purelib'])")" 6 | PATTERN="$SITE_PACKAGES/$MODULE_NAME*" 7 | 8 | # Remove from site_packages 9 | for path in $PATTERN; do 10 | if [ -e "$path" ]; then 11 | echo "Removing $path" 12 | rm -r "$path" 13 | fi 14 | done 15 | 16 | # Remove launcher scripts 17 | filenames=("/usr/bin/nwg-hello") 18 | 19 | for filename in "${filenames[@]}"; do 20 | rm -f "$filename" 21 | echo "Removing -f $filename" 22 | done 23 | 24 | rm -rf /etc/nwg-hello 25 | rm -rf /usr/share/nwg-hello 26 | 27 | rm -f /usr/share/licenses/$PROGRAM_NAME/LICENSE 28 | rm -f /usr/share/doc/$PROGRAM_NAME/README.md 29 | 30 | echo "Remember to remove nwg-hello from /etc/greetd/config.toml or delete /etc/greetd/greetd.conf" 31 | --------------------------------------------------------------------------------