I’ve used Linux for a while now, and after ~15 years I think I’m finally happy with how I’m handling
- My login
- My environmental variables
- My daemons
This ( article ? blog ? ) is going to go over my personal understanding of how, in what order, and why things happen when I login to my graphical session.
I will be going over my setup using Greetd and tuigreet to launch River or Sway, making sure my environments are properly setup, as well as how I handle my daemons such as Mako, Gammastep and foot.
I use Arch btw, which uses Systemd as it’s init system by default. Though some of this will probably be useful regardless of the init system you are using.
If you’ve played around with any sort of minimal, non-desktop environment, setups, you’ve almost certainly run into the problem of needing to set some specific environmental variables for things to work correctly.
Like the MOZ_WAYLAND_ENABLE for example.
If you’re like me, you simply placed export MOZ_WAYLAND_ENABLE=1 line in your shell’s rc file.
It worked fine because you were still launching sway from your shell after login in using the default [email protected] with tiny writing on your High DPI display.
A couple days of configuration later, you decide to try out a login manager. You set it up, it’s great, it’s pretty, it launches your compositor for you. Life seems great…
But then you open Firefox, and you witness the horror. What once was your beautiful tridactyl workspace has become an ugly pixelated mess of badly anti-aliased fonts and improperly scaled UI.
Okay ? Ah yes ! I use fractional scaling !
Hmm… immediately :
you open a terminal,
you type xprop —
as you hover your cursor over what can barely be called a window,
you realize you already know what will happen…
the forever feared X11 cross appears before you.
Okay, keep calm, then :
after enjoying the wonderful orange fox displayed in all of its caca glory,
you frenetically type echo $MOZ… <Tab>… oh no… it autocompletes.
You press <Enter>, a part of you still hoping to see a 0.
Hope seemed lost, but you know your stuff :
“I’ll just set it when I open Firefox in my sway config !”
It works for a while, you’ve almost forgotten about that unpleasant experience. But one day you open a link from an application, and that day, Firefox wasn’t running, our favorite browser launches, again looking like he was bullied by Chrome into limiting its resolution to 480p.
Right… lets fix this.
I used Sway for a long time, but my current favorite compositor is River, and it is the one I use. I’ll be using it in my examples.
There are three places I need environmental variables to be set ( as far as I am aware ) :
- Whenever I’m in my shell, both in my graphical session and in the tty
- For my compositor and its children
- For my Systemd session services
There are many variables that I need, but thankfully, the only ones I need to worry about are the ones I set manually
( well, almost… Damn you WAYLAND_DISPLAY ! I’ll deal with you later though… ).
So, here is what I need :
EDITOR=nvim- 😎PATH="$HOME/.local/bin:$PATH"- I want my scripts to be in myPATH- Wayland stuff :
XDG_SESSION_TYPE=waylandGDK_BACKEND=waylandQT_QPA_PLATFORM=waylandMOZ_ENABLE_WAYLAND=1
- Desktop stuff :
XDG_SESSION_DESKTOP=riverXDG_CURRENT_DESKTOP=river
Variables like HOME, XDG_RUNTIME_DIR and DBUS_SESSION_BUS_ADDRESS are handled by PAM.
So they won’t make too much of an appearance in this ( article ? blog ? ).
Now : what where ?
EDITOR and PATH need to always be set for me.
That is to say, whenever I’m in an interactive shell, graphical session or not.
They should also be part of my compositor’s environment so that I can access and use them in my config,
and they can be inherited by apps launched by my compositor.
All the Wayland related variables need to be in my compositor’s environment. As well as in my shell, but only when I’m in my graphical session. Luckly, I ( and almost definitely you ) launch my terminal emulator from my compositor. This means that once we have them in the compositor’s environment, we don’t have to worry about conditionally needing it in the shell.
The XDG desktop variables have more or less the same requirements as the Wayland ones, but they are dependent on the compositor, so let’s keep a distinction between them.
And that’s it really, I do have some more obscure variables for specific things, but they all fit in those three situations :
- shell + compositor
- compositor
- compositor ( dependent )
So let’s group them like that in some POSIX shell scripts ( because I like POSIX ) :
~/.config/env/core - the variables I always need.
#!/bin/sh
export PATH="$HOME/.local/bin:$PATH"
export EDITOR=nvim~/.config/env/compositor - the variables I need in my compositor.
#!/bin/sh
# The variables I need in my compositor
[ -f "${XDG_CONFIG_HOME:-$HOME/.config}/env/core" ] && . "${XDG_CONFIG_HOME:-$HOME/.config}/env/core"
export XDG_SESSION_TYPE=wayland
export GDK_BACKEND=wayland
export QT_QPA_PLATFORM=wayland
export MOZ_ENABLE_WAYLAND=1Here I am sourcing my core variables inside my compositor variables script,
and just to avoid annoying file does not f****** exist errors,
I make sure the file exists first.
As you can see, I’m using ${XDG_CONFIG_HOME:-$HOME/.config},
this means that I will use $XDG_CONFIG_HOME and default to $HOME/.config if it is not set.
For most people, simply using $HOME/.config would work,
but for the sake of portability,
this ensures that if XDG_CONFIG_HOME is set to a custom value
( usually early on by PAM ),
that we look for the configs in the right place.
We can install both packages and enable greetd.service,
I personally like Greetd because of it’s very simple config :
/etc/greetd/config.toml - mmmmm… TOML <3
[terminal]
vt = 1
[default_session]
command = "tuigreet --session-wrapper compositor-wrapper"
user = "greeter"
Nothing crazy here :
vttells Greetd which virtual terminal to start oncommandis the greeter command run by Greetduseris the user that will run the greeter command ; this should be left as the defaultgreeterunless you know what you are doing. It should not be your username
Now looking at the greeter command itself,
we notice that we do not have the --cmd option like we usually see in example Greetd configs ;
though we do have this --session-wrapper option.
tuigreet will look for available Wayland sessions in /usr/share/wayland-sessions,
these will be selectable at when on the login screen.
The session wrapper is an executable ( in our case, a POSIX script, because I like POSIX ),
tuigreet will run this executable and pass in the selected session’s Exec as arguments when login in.
For example, let’s have a look at /usr/share/wayland-sessions/river.desktop - this should already exist if you have River installed.
[Desktop Entry]
Name=River
Comment=A dynamic tiling Wayland compositor
Exec=river
Type=Application
The Exec is set to river so when login in,
the actual command that will be run by our greeter is
compositor-wrapper river
when the River session is selected.
This is nice because the wrapper script will be run for any Wayland session I chose on the login screen.
So what does this script look like ?
/usr/local/bin/compositor-wrapper - /usr/local/bin is in PATH by default, this is also the suggested directory on the Greetd website.
#!/bin/sh
[ -f "${XDG_CONFIG_HOME:-$HOME/.config}/env/compositor" ] && . "${XDG_CONFIG_HOME:-$HOME/.config}/env/compositor"
systemd-cat -t compositor "$@"
Yep, that’s it. Very simple. All we are doing is sourcing our compositor variables the same way we did with the core variables earlier. We don’t have to source the core variables because they are sourced in the compositor variables script. Then we run the compositor, there are a few things to unpack here :
Firstly, what is "$@" ? its a magic variable that corresponds to the arguments passed to the script.
In our case, its simply river.
Our script could simply have "$@", or exec "$@",
but I decided to use systemd-cat -t compositor "$@" to redirect my compositor’s log to the journal,
this allows me to easily view them with journalctl -t compositor.
We’re almost there !
How does this look on a sequence diagram you ask ?
sequenceDiagram
participant pam as PAM
participant greetd as Greetd service
participant tuigreet
actor user as The user
participant compositor as Compositor
participant wrapper as Wrapper script
participant compositorvars as Compositor variables script
participant corevars as Core variables script
Note over pam,greetd: The system boots, system<br/>services are started
activate pam
activate greetd
loop
greetd-)pam: Please start a session for<br/>`greeter` with tuigreet
Note over pam: The `greeter` session is created
activate pam
pam-)+tuigreet: Starts tuigreet
Note over tuigreet: We are inside the `greeter` session
tuigreet->>user: Displays login screen
user-)tuigreet: Enters login info for `username`
tuigreet->>+greetd: Please start a session for<br/>`username` with the wrapper
greetd--)-tuigreet: Not if you don’t give me the correct password
Note over greetd,tuigreet: Auth is actually much more complex, and I am only<br/>showing what we need to know kinda goes down.
tuigreet->>+user: Displays a password incorrect message
user-)tuigreet: Enters the password again<br/>but correctly this time
tuigreet->>+greetd: Please start a session for<br/>`username` with the wrapper
greetd--)-tuigreet: Sure, will do !<br/>( but he doesn’t do it ! yet… )
tuigreet--)-pam: Exits
Note over pam: The `greeter` session is destroyed
deactivate pam
Note over greetd: Greetd notices his greeter has died<br/>and will grant his last request
greetd-)pam: Please start a session for<br/>`username` with the wrapper
Note over pam: The `username` session is created
activate pam
Note over pam: From this point on, the other<br/>diagrams start and run in parallel
pam-)+wrapper: Launch the wrapper as `username`
Note over wrapper: We are inside the `username` session
wrapper->>+compositorvars: Source compositor variables script
compositorvars->>+corevars: Source core variables script
corevars--)-compositorvars: Exits
compositorvars--)-wrapper: Exits
wrapper->>+compositor: Start the compositor
deactivate wrapper
Note over compositor: The XDG Desktop vars are set
Note over compositor: We have access to all our variables here !
compositor-)user: Displays GUI
Note over user,compositor: More stuff happens, some can<br/>be seen in the other diagrams
user-)compositor: Press logout keybind
compositor--)-pam: Exits
Note over pam: The `username` session is destroyed
deactivate pam
Note over greetd: Greetd notices the session has<br/>stopped and restarts the loop
end
deactivate pam
deactivate greetd
This is where things start getting a little annoying.
When the user session is created,
PAM also starts a separate user session for Systemd session units
( that’s the units you deal with when using systemctl --user ).
You should be able to see both sessions with the loginctl command.
$ loginctl
SESSION UID USER SEAT LEADER CLASS TTY IDLE SINCE
4 1000 lmaooo - 801 manager - no -
7 1000 lmaooo seat0 2604 user tty1 no -
2 sessions listed.
The session that has the luxurious possibility to sit down is the session your compositor is running in. The other one is the one Systemd is able to do stuff in.
The Systemd session has the same privileges because it uses the same user. But it has its own environment.
Thankfully, both sessions are provided with all the basic environmental variables we need,
thank you PAM, when they are created.
But here is the catch, WAYLAND_DISPLAY is set in the compositor’s environment.
And this happens after the sessions were initialized by PAM.
And why do we need this variable ?
Well, it’s because we have services that neet to interact with our compositor,
they do this through a socket.
The WAYLAND_DISPLAY variable gives us the name of that socket.
The Wayland socket has a predictable path,
it’s "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY"
which is likely /run/user/1000/wayland-0
or /run/user/1000/wayland-1.
Here is the plan :
let’s create a Systemd unit
that watches the XDG runtime directory.
If anything matches wayland-*,
store it in WAYLAND_DISPLAY and make it accessible to Systemd units in the session.
We start with the Systemd path file.
~/.config/systemd/user/wayland-env.path
[Unit]
Description=Watch for new Wayland display socket(s) and trigger import
[Path]
PathExistsGlob=%t/wayland-*
Unit=wayland-env.service
[Install]
WantedBy=default.targetI won’t go all the syntax, but I’ll explain what the important lines do.
PathExistsGlob- here%texpands to the XDG runtime directory.Unit- this is the unit that is triggered when the path exists.WantedBy- the[Install]section contains constraints to apply when the file is enabled (systemctl --user enable wayland-env.path)
default.target is a Systemd target.
If I understand correctly, this will trigger as soon as Systemd starts.
When a unit Wants another unit, it will attempt to start it when it starts.
For us, it means that we will start watching for the socket as soon as Systemd starts.
Let’s have a look at our Systemd service file :
~/.config/systemd/user/wayland-env.service
[Unit]
Description=Import WAYLAND_DISPLAY into the user systemd environment
Wants=graphical-session.target
Before=graphical-session.target
After=default.target
[Service]
Type=oneshot
RemainAfterExit=true
ExecStart=/bin/sh -c '\
socket="$$(ls -1t "%t"/wayland-* 2>/dev/null | head -n1)"; \
: "$${socket:?No Wayland socket found}"; \
export WAYLAND_DISPLAY="$${socket##*/}"; \
/usr/bin/systemctl --user import-environment WAYLAND_DISPLAY'
ExecStop=/usr/bin/systemctl --user unset-environment WAYLAND_DISPLAYWe see Wants straid away, which means that when we start,
we try to start graphical-session.target too.
Before and After tells us to wait until default.target,
and then to only start graphical-session.target after we have finished.
This target we are starting will start units that require it. For me that is Mako, Gammastep when it’s enabled, and the Foot server.
Just a sidenote, Foot is my terminal emulator. It has the ability to run in a daemon mode.
All three of these behave differently, but I’ll go over that after.
Now the [Service] section :
Type=oneshot- the process will run once or something. Read the docs if you want a better explanation.RemainAfterExit=true- means the unit will still be in an active state after the process exits. This is important so that our Path file doesn’t try to launch it again.ExecStartandExecStopare the processes to run when starting and stopping the service. In practice,ExecStopmight not be very useful because our service won’t stop being active. This line simply means that if you stop the service manually, withsystemctl --user stop wayland-env.servicefor example, it would run the process.
ExecStart runs /bin/sh, our POSIX shell,
-c is basically allowing us to put our script inline as an argument.
Here it is cleaned up a little :
socket="$(ls -1t "/run/user/1000"/wayland-* 2>/dev/null | head -n1)"
: "${socket:?No Wayland socket found}"
export WAYLAND_DISPLAY="${socket##*/}"
/usr/bin/systemctl --user import-environment WAYLAND_DISPLAYNotice that I’ve replaced %t with /run/user/1000,
this is to show that it is expanded by Systemd before being used by /bin/sh.
This %" would not be expanded in a script.
Also, to avoid Systemd trying to expand our variables,
we had to escape the $ with $$.
And finally, using ; \, so we can avoid having it all on a single line.
I’ll go through it quickly :
- we list the Wayland sockets one item per line,
we order them so that the most recent is on top (
ls -1t), we ignorels’s errors (2>/dev/null), and finally we get the first line (head -n1) and put it insocket. - We exit with an error if
socketis empty. Here,:simply forces the evaluation of our string, makingshexit ifsocketis empty (${...:?error message}). - We export
WAYLAND_DISPLAYwith the basename (${...##*/}) of the socket as its value. - We make it available to other Systemd units !
That’s it. We should have everything working.
Gammastep will launch as soon as your compositor is open ( if it is enabled ).
Mako will be launched by DBus when it is needed.
Foot can be socket activated,
assuming foot-server.socket is enabled,
the Foot server will be launched when you first run footclient.
Let’s look at Gammastep on a diagram :
sequenceDiagram
participant compositor as Compositor
participant waysock as Wayland socket
participant path as Our path file
participant service as Our service file
participant dt as default.target
participant gst as graphical-session.target
participant gammastep as Gammastep
activate dt
dt->>+path: Oi, wake up.
deactivate dt
Note over path: Starts watching for the<br/>Wayland socket
Note over compositor: The compositor opens
activate compositor
compositor-)+waysock: Creates the socket
Note over path: Sees the socket
path-)+service: Oi, wake up.
activate service
Note over service: Gets and sets `WAYLAND_DISPLAY`
service-)-gst:
activate gst
Note over service: Stays active even if the process has ended
gst-)+gammastep: Oi, wake up.
Note over gammastep: Wakes up and has access to `WAYLAND_DISPLAY`
gammastep--)gst: Okay, okay
deactivate gst
gammastep-)waysock: Be more orange
waysock-)compositor: Be more orange
Note over compositor: Becomes more orange
deactivate compositor
deactivate path
deactivate service
deactivate gammastep
deactivate waysock
This is why we specify Before=graphical-session.target in our service file.
So Gammastep doesn’t start running until we have made WAYLAND_DISPLAY accessible.
Mako is activated by DBus, it goes something like :
sequenceDiagram
actor user as The user
participant compositor as Compositor
participant dbus as DBus
participant mako as Mako
activate compositor
activate dbus
Note over mako: At this point, `WAYLAND_DISPLAY` has already<br/>been made accessible by our unit.
user-)compositor: Presses keybind to see a notification
compositor-)+dbus: Notification please
dbus->>+mako: Oi, wake up.
mako--)dbus: I’m awake
dbus-)+mako: There’s a notification to show
deactivate dbus
mako-)-compositor: Draw this notification on the screen please
Note over mako: Stays awake for future notifications
compositor-)user: Displays the notification
user-)compositor: Presses keybind to see a notification
compositor-)+dbus: Notification please
dbus-)-mako: There’s a notification to show
activate mako
mako-)-compositor: Draw this notification on the screen please
compositor-)user: Displays the notification
deactivate mako
deactivate compositor
deactivate dbus
Foot is socket activated, so its unit has two parts ( like our unit ! ) :
- A service file
- A
systemd.socketfile, this creates a socket ( if enabled ) aftergraphical-session.target. It then launches the service file when somebody sends something to the socket.
sequenceDiagram
actor user as The user
participant compositor as Compositor
participant gst as graphical-session.target
participant footclient as Foot client process
participant footsock as Foot socket
participant foot as Foot server
participant foot.sock as Foot socket watcher
activate compositor
Note over compositor,gst: The compositor has finished loading and `WAYLAND_DISPLAY`<br/>has been made accessible to services.
activate gst
gst-)+foot.sock: Oi, wake up.
activate foot.sock
foot.sock-)footsock: Creates socket
activate footsock
foot.sock--)-gst: I’m awake, I’m awake
deactivate gst
user-)compositor: Presses keybind to<br/>launch `footclient`
compositor-)+footclient: Runs footclient
footclient->>+footsock: I want to talk with the server
Note over foot.sock: Notices that something happened in the socket
foot.sock-)+foot: Oi, wake up.
foot->>+footsock: Any news ?
footsock--)-foot: A client wants to talk
foot-)footsock: I’m listening
footsock--)-footclient: He’s listening
footclient-)compositor: Please display the terminal
compositor-)user: Displays the terminal window
deactivate foot
deactivate footclient
deactivate foot.sock
deactivate footsock
deactivate compositor
I want pretty colours and a nice font when I’m login in. Even if I’m in a TUI.
I also want to avoid my kernel info messages being printed directly on top of my login screen.
But I don’t want to turn my kernel quiet.
Let’s fix this with Cage. Basically, instead of running tuigreet in the virtual terminal that has limited capabilities we open cage, we open Foot ( in standalone mode, not server-client mode ) inside of cage, and we run tuigreet in Foot.
Here is the greetd config that I use :
/etc/greetd/config.toml - <3
[terminal]
vt = 1
[default_session]
command = "cage -s -- foot -F -f monospace:size=14 -- tuigreet --session-wrapper compositor-wrapper -r --remember-session --asterisks -g 'おはよう〜' -t --theme 'border=magenta;text=cyan;prompt=green;time=red;action=blue;button=yellow;container=black;input=red'"
user = "greeter"
Important : If for some reason you decide not to use Cage’s -s option,
you cannot use Control+Alt+F2 to switch to the second virtual terminal.
If for some reason your compositor fails to load,
you will be stuck in a login loop.
Don’t worry ! You can press F2 inside tuigreet to set the command to launch after login.
You can set this to /bin/bash or /bin/zsh ( bash or zsh should be sufficient ).
And tuigreet will log you into your virtual terminal with your shell.
When login out and in quickly ( we’re talking a couple seconds ),
the manager session does not reset
( I don’t know why, just my observation ).
This doesn’t cause much of a problem unless the Wayland socket changes.
Because the manager session hasn’t reset,
it doesn't update WAYLAND_DISPLAY and things get a little sad.
This will almost never happen, but it is possible.
It happened for me when switching between River and Cage,
they use wayland-0 and wayland-1 by default.
If this is something that you would come across often I suggest these updated unit files :
~/.config/systemd/user/wayland-env.service
[Unit]
Description=Update WAYLAND_DISPLAY in the user systemd environment
Wants=graphical-session.target
Before=graphical-session.target
After=default.target
[Service]
Type=notify
ExecStart=%E/scripts/watch-wayland
NotifyAccess=all
Restart=on-failure~/.config/scripts/watch-wayland
#!/bin/sh
dir=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}
update() {
socket=$(find "$dir" -maxdepth 1 -type s -name 'wayland-*' -print0 |
xargs -0 -r stat -c '%Y %n' |
sort -n |
tail -n 1 |
cut -d' ' -f2-)
current=$(systemctl --user show-environment 2>/dev/null |
sed -n 's/^WAYLAND_DISPLAY=//p')
if [ -z "$socket" ]; then
if [ -n "$current" ]; then
systemctl --user unset-environment WAYLAND_DISPLAY
fi
else
name=${socket##*/}
if [ "$name" != "$current" ]; then
systemctl --user set-environment WAYLAND_DISPLAY="$name"
fi
fi
}
update
if command -v systemd-notify >/dev/null 2>&1; then
systemd-notify --ready
fi
while inotifywait -e create,delete -q "$dir" --include "^$dir/wayland-[0-9]+$"; do
update
doneI’m not going to go over exactly what I changed,
but this should keep updating WAYLAND_DISPLAY every time a socket is created or deleted.
I use ZSH,
so I also set variables in my ~/.zshenv and ~/.zshrc.
Actually, I don’t currently define any in my ~/.zshrc,
which is sourced whenever you are in an interactive shell.
And I simply source my core variables in my ~/zshenv.
This one is always sourced by ZSH.
XDG_SESSION_DESKTOP=river and XDG_CURRENT_DESKTOP=river
are also set in my ~/.config/river/init.
And for those who want to make sure they have the setup I have :
- I have the packages
mako,gammastep,foot( andfoot-terminfo),greetd,tuigreet,riverandcage. - I have enabled my path file, Gammastep and Foot’s socket activation with
systemctl --user enable wayland-env.path gammastep foot-server.socket. - I have enabled Greetd with
sudo systemctl enable greetd.
Thanks for reading and happy configurating !