├── .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 | 
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 | [](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 |
46 |
--------------------------------------------------------------------------------
/img/poweroff-default.svg:
--------------------------------------------------------------------------------
1 |
2 |
53 |
--------------------------------------------------------------------------------
/img/reboot-default.svg:
--------------------------------------------------------------------------------
1 |
2 |
60 |
--------------------------------------------------------------------------------
/img/sleep-default.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
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 |
--------------------------------------------------------------------------------