Provides graphical session with environment management, XDG autostart support, and clean shutdown by wrapping standalone Wayland compositors into a set of systemd units.
Important
This project is currently in a stable phase with a slow-burning refactoring.
Although no drastic changes are planned, keep an eye for commits with breaking
changes, indicated by an exclamation point (e.g. fix!: ...
, chore!: ...
,
feat!: ...
, etc.).
Important
uwsm-bin-name
build option is considered deprecated and will be removed in
future versions.
Note
It is highly recommended to use
dbus-broker as the D-Bus daemon
implementation. Among other benefits, it reuses the systemd activation
environment instead of having its own separate one. This simplifies
environment management and allows proper cleanup. The separate activation
environment of the reference D-Bus implementation doesn't allow unsetting
vars, so they're set to an empty string instead, as a best effort cleanup. The
only way to properly clean up in this case is to run
loginctl terminate-user ""
.
Uses systemd units and dependencies for startup, operation, and shutdown.
- Binds to the basic
structure
of
graphical-session-pre.target
,graphical-session.target
,xdg-desktop-autostart.target
. - Aadds custom nested slices
app-graphical.slice
,background-graphical.slice
,session-graphical.slice
to put apps in and terminate them cleanly on exit. - Provides convenient way of launching apps to those slices.
Systemd units are treated with hierarchy and universality in mind.
- Templated units with specifiers.
- Named from common to specific where possible.
- Allowing for high-level
name-.d
drop-ins.
Compositor-specific behavior is adjustable by plugins.
Currently included:
sway
wayfire
labwc
hyprland
Idempotently (well, best-effort-idempotently) handles environment.
- On startup environment is prepared by:
- sourcing shell profile
- sourcing
uwsm-env
,uwsm-env-${desktop}
files from each dir of reversed${XDG_CONFIG_HOME}:${XDG_CONFIG_DIRS}
(in increasing priority), where${desktop}
is each item of${XDG_CURRENT_DESKTOP}
, lowercased
- Difference between environment state before and after preparation is exported into systemd user manager (and dbus activation environment if reference dbus implementation is used)
- On shutdown previously exported variables are unset from systemd user manager (activation environment of reference dbus daemon does not support unsetting, so those vars are emptied instead (!))
- Lists of variables for export and cleanup are determined algorithmically by:
- comparing environment before and after preparation procedures
- boolean operations with predefined lists
- manually exported vars by
uwsm finalize
action
Can work with Desktop entries from `wayland-sessions` in XDG data hierarchy and/or be included in them.
- Actively select and launch compositor from Desktop entry (which is used as
compositor instance ID):
- Data taken from entry (Can be amended or overridden via cli arguments):
Exec
for argument listDesktopNames
forXDG_CURRENT_DESKTOP
andXDG_SESSION_DESKTOP
Name
andComment
for unitDescription
- Entries can be overridden, masked or added in
${XDG_DATA_HOME}/wayland-sessions/
- Optional interactive selector (requires whiptail), choice is saved in
${XDG_CONFIG_HOME}/uwsm-default-id
- Desktop entry actions are supported
- Data taken from entry (Can be amended or overridden via cli arguments):
- Be launched via a Desktop entry by a login/display manager.
Can run with arbitrary compositor command line, or take it (along with other data) from desktop entries (saved as a unit drop-in).
wayland-wm-env@${compositor}.service.d/50_custom.conf
wayland-wm@${compositor}.service.d/50_custom.conf
Provides better control of XDG autostart apps.
- XDG autostart services (
app-*@autostart.service
units) are placed intoapp-graphical.slice
that receives stop action before compositor is stopped. - Can be mass-controlled via stopping and starting
wayland-session-xdg-autostart@${compositor}.target
Tries best to shutdown session cleanly via a net of dependencies between units.
All managed transient files (in /run/user/${UID}/systemd/user
):
background-graphical.slice
app-graphical.slice
session-graphical.slice
app-@autostart.service.d/slice-tweak.conf
wayland-session-pre@.target
wayland-session-shutdown.target
wayland-session-xdg-autostart@.target
wayland-session@.target
wayland-wm-app-daemon.service
wayland-wm-env@.service
wayland-wm-env@${compositor}.service.d/50_custom.conf
wayland-wm@.service
wayland-wm@${compositor}.service.d/50_custom.conf
See Longer story section below for descriptions.
Provides helpers and tools for various operations.
uwsm finalize
: for finalizing service startup (compositor service unit usesType=notify
) and exporting variables set by compositoruwsm check may-start
: for checking conditions for launch at login (for integration into login shell profile)uwsm app
: for launching applications as scopes or services in proper slices- desktop entries or plain executables are supported
- support for launching a terminal/in terminal (proposed xdg-terminal-exec)
- flexible unit metadata support
uwsm-app
: a simple and fast shell client to app-daemon feature of uwsm, a drop-in replacement ofuwsm app
. The daemon (started on-demand) handles finding requested desktop entries, parsing and generation of commands for client to execute. This avoids the overhead of repeated python startup and increases app launch speed.uuctl
: a graphical tool for managing user units (uses dmenu-like menus).
Building and installing the python project directly.
meson setup --prefix=/usr/local -Duuctl=enabled -Duwsm-app=enabled build
meson install -C build
The example enables optional tools uuctl
and uwsm-app
available in this
project (see helpers and tools spoiler in
concepts section above).
Building and installing a deb package.
IFS='()' read -r _ current_version _ < debian/changelog
sudo apt install devscripts
mk-build-deps
sudo apt install --mark-auto ./uwsm-build-deps_${current_version}_all.deb
dpkg-buildpackage -b -tc --no-sign
sudo apt install ../uwsm_${current_version}_all.deb
Arch AUR package.
Nix flake/derivation.
Ensure your compositor runs uwsm finalize
command at the end of its startup.
If compositor sets any useful environment variables, list their names as
arguments.
Details
- It fills systemd and dbus environments with essential vars set by compositor:
WAYLAND_DISPLAY
,DISPLAY
- Any additional vars can be given as arguments by name.
- Undefined vars are silently ignored.
- Any exported variables are also added to cleanup list.
- If environment export is successful, it signals compositor service readiness,
so
graphical-session.target
can properly be declared reached. If this stage fails, the compositor will be terminated in 10 seconds.
Example snippet for sway config:
exec exec uwsm finalize SWAYSOCK I3SOCK XCURSOR_SIZE XCURSOR_THEME
To properly put applications in app-graphical.slice
(or like), Configure
application launching in compositor via:
uwsm app -- {executable|entry.desktop[:action]} [args ...]
When app launching is properly configured, compositor service itself can be
placed in session.slice
by either:
- Setting environment variable
UWSM_USE_SESSION_SLICE=true
before generating units. Best places to put this:- export in
~/.profile
beforeuwsm
invocation - put in
~/.config/environment.d/*.conf
(seeman environment.d
)
- export in
- Adding
-S
argument touwsm start
subcommand.
Background and details
By default uwsm
launhces compositor service in app.slice
and all processes
spawned by compositor will be a part of wayland-wm@${compositor}.service
unit.
This works, but is not an optimal solution.
Systemd
documentation
recommends running compositors in session.slice
and launch apps as scopes or
services in app.slice
.
uwsm
provides convenient way of handling this: it generates special nested
slices that will also receive stop action ordered before
wayland-wm@${compositor}.service
shutdown:
app-graphical.slice
background-graphical.slice
session-graphical.slice
app-*@autostart.service
units are also modified to be started in
app-graphical.slice
.
To launch an app inside one of those slices, use:
uwsm app [-s a|b|s|custom.slice] [-t scope|service] -- your_app [with args]
Launching desktop entries via a valid ID is also supported, (optionally with an action ID appended via ':'):
uwsm app [-s a|b|s|custom.slice] [-t scope|service] -- your_app.desktop[:action] [with args]
In this case args must be supported by the entry or its selected action according to XDG Desktop Entry Specification.
Specifying paths to executables or desktop entry files is also supported.
Always use --
to disambiguate command line if any dashed arguments are
intended for the app being launched.
Example snippets for sway config for launching apps:
Launch proposed default terminal:
bindsym --to-code $mod+t exec exec uwsm app -T
Fuzzel has a very handy launch-prefix option:
bindsym --to-code $mod+r exec exec fuzzel --launch-prefix='uwsm app --' --log-no-syslog --log-level=warning
Launch SpaceFM via a desktop entry:
bindsym --to-code $mod+e exec exec uwsm app spacefm.desktop
Featherpad desktop entry has "standalone-window" action:
bindsym --to-code $mod+n exec exec uwsm app featherpad.desktop:standalone-window
Unit type of launched apps can be controlled by -t service|scope
argument or
setting its default via UWSM_APP_UNIT_TYPE
env var.
-h|--help
option is available for uwsm
and all of its subcommands.
Basics:
uwsm start [options] -- ${compositor} [arguments]
Always use --
to disambiguate command line if any dashed arguments are
intended for launched compositor.
${compositor}
can be an executable or a valid
desktop entry ID
(optionally with an
action ID
appended via ':'), or one of special values: select|default
Optional parameters to provide more metadata:
-[a|e]D DesktopName1[:DesktopMame2:...]
: append (-a
) or exclusively set (-e
)${XDG_CURRENT_DESKTOP}
-N Name
-C "Compositor description"
Arguments and metadata are stored in specifier unit drop-ins if needed.
Some details
uwsm start [-[a|e]D DesktopName1[:DesktopMame2:...]] [-N Name] [-C "Compositor description"] -- ${compositor} [with "any complex" --arguments]
If ${compositor}
is a desktop entry ID, uwsm
will get desktop entry from
wayland-sessions
data hierarchy, Exec
will be used for command line, and
DesktopNames
will fill $XDG_CURRENT_DESKTOP
, Name
and Comment
will go to
units' descriptons.
Arguments provided on command line are appended to the command line of session
desktop entry (unlike application entries), no argument processing is done
(Please
file a bug report if
you encounter any wayland-sessions desktop entry with %
-fields which would
require this behavior to be altered).
If you want to customize compositor execution provided with a desktop entry,
copy it to ~/.local/share/wayland-sessions/
and change to your liking,
including adding
actions.
If ${compositor}
is select
or default
, uwsm
invokes a menu to select
desktop entries available in wayland-sessions
data hierarchy (including their
actions). Selection is saved, previous selection is highlighted (or launched
right away in case of default
). Selected entry is used as instance ID.
There is also a separate select
action (uwsm select
) that only selects and
saves default ${compositor}
and does nothing else, which is handy for seamless
shell profile integration.
When started, uwsm
will wait while wayland session is running, and terminate
session if is itself interrupted or terminated.
To launch automatically after login on virtual console 1, if systemd is at
graphical.target
, add this to shell profile:
if uwsm check may-start && uwsm select; then
exec uwsm start default
fi
check may-start
checker subcommand, among other things, screens for being in
interactive login shell, which is essential, since profile sourcing can
otherwise lead to nasty loops.
select
shows whiptail menu to select default desktop entry from
wayland-sessions
. At this point one can cancel and continue to the normal
login shell.
start default
launches the previously selected default compositor.
exec
in shell profile causes uwsm
to replace login shell, binding it to
user's login session.
To launch uwsm from a display/login manager, uwsm
can be used inside desktop
entries. Example /usr/local/share/wayland-sessions/my-compositor.desktop
:
[Desktop Entry]
Name=My compositor (with UWSM)
Comment=My cool compositor
Exec=uwsm start -N "My compositor" -D mycompositor -C "My cool compositor" mywm
DesktopNames=mycompositor
Type=Application
Things to keep in mind:
- For consistency, command line arguments should mirror the keys of the entry
- Command in
Exec=
should start withuwsm start
- It should not launch a desktop entry, only an executable.
Potentially such entries may be found and used by uwsm
itself, i.e. in shell
profile integration situation, or when launched manually. Following the
principles above ensures uwsm
will properly recognize itself and parse
requested arguments inside the entry without any side effects.
Testing and feedback is needed.
Either of:
loginctl terminate-user ""
(this ends all login sessions and units of current user, good for resetting everything, including runtime units, environments, etc.)loginctl terminate-sesion "$XDG_SESSION_ID"
(this ends current login session, uwsm in this session will bring down graphical session units before exiting. Empty argument will only work if loginctl is called from session scope itself)uwsm stop
(brings down graphical session units. Login session will end ifuwsm start
replaces login shell)systemctl --user stop wayland-wm@*.service
(effectively the same as previous one)
Some extended examples and partial recreation of some behaviors via excessive shell code, just for deeper explanation.
Dive
(At least for now) units are generated by the script.
Run uwsm start -o ${compositor}
to populate ${XDG_RUNTIME_DIR}/systemd/user/
with them and do nothing else (-o
).
Any remainder arguments are appended to compositor argument list (even when
${compositor}
is a desktop entry). Use --
to disambigue:
uwsm start -o -- ${compositor} with "any complex" --arguments
Desktop entries can be overridden or added in
${XDG_DATA_HOME}/wayland-sessions/
.
Basic set of generated units:
- templated targets boud to stock systemd user-level targets
wayland-session-pre@.target
wayland-session@.target
wayland-session-xdg-autostart@.target
- templated services
wayland-wm-env@.service
- environment preloader servicewayland-wm@.service
- main compositor servicewayland-wm-app-daemon.service
- fast app command generator
- slices for apps nested in stock systemd user-level slices
app-graphical.slice
background-graphical.slice
session-graphical.slice
- tweaks
wayland-wm-env@${compositor}.service.d/custom.conf
,wayland-wm@${compositor}.service.d/custom.conf
- if arguments and/or various names were given on command line, they go here.app-@autostart.service.d/slice-tweak.conf
- assigns XDG autostart apps toapp-graphical.slice
- shutdown and cleanup target
wayland-session-shutdown.target
- conflicts with operational units. Triggered by the end ofwayland-wm*.service
units for more robust cleanup, including on failures. But can also be called manually for shutdown.
After units are generated, compositor can be started by:
systemctl --user start wayland-wm@${compositor}.service
Add --wait
to hold terminal until session ends.
exec
it from login shell to bind to login session:
exec systemctl --user start --wait wayland-wm@${compositor}.service
Still if login session is terminated, wayland session will continue running, most likely no longer being accessible.
To also bind it the other way around, shell traps are used:
trap "if systemctl --user is-active -q wayland-wm@${compositor}.service ; then systemctl --user --stop wayland-wm@${compositor}.service ; fi" INT EXIT HUP TERM
This makes the end of login shell also be the end of wayland session.
When wayland-wm-env@.service
is started during graphical-session-pre.target
startup, uwsm aux prepare-env ${compositor}
is launched (with shared set of
custom arguments).
It runs shell code to prepare environment, that sources shell profile,
uwsm-env*
files, anything that plugins dictate. Environment state at the end
of shell code is given back to the main process. uwsm
is also smart enough to
find login session associated with current TTY and set $XDG_SESSION_ID
,
$XDG_VTNR
.
The difference between initial env (that is the state of activation environment)
and after all the sourcing and setting is done, plus varnames.always_export
,
minus varnames.never_export
, is added to activation environment of systemd
user manager and dbus.
Those variable names, plus varnames.always_cleanup
minus
varnames.never_cleanup
are written to a cleanup list file in runtime dir.
wayland-wm@.service
uses Type=notify
and waits for compositor to signal
started state. Activation environments will also need to receive essential
variables like WAYLAND_DISPLAY
to launch graphical applications successfully.
uwsm finalize [VAR [VAR2...]]
runs:
dbus-update-activation-environment --systemd WAYLAND_DISPLAY DISPLAY [VAR [VAR2...]]
systemctl --user import-environment WAYLAND_DISPLAY DISPLAY [VAR [VAR2...]]
systemd-notify --ready
The first two together might be an overkill.
Only defined variables are used. Variables that are not blacklisted by
varnames.never_cleanup
set are also added to cleanup list in runtime dir.
Just stop the main service:
systemctl --user stop "wayland-wm@${compositor}.service"
, everything else will
stopped by systemd.
Wildcard systemctl --user stop "wayland-wm@*.service"
will also work.
If start command was run with exec
from login shell or .profile
, this stop
command also doubles as a logout command.
When wayland-wm-env@${compositor}.service
is stopped, uwsm aux cleanup-env
is launched. It looks for any cleanup files (env_names_for_cleanup_*
) in
runtime dir. Listed variables, plus varnames.always_cleanup
minus
varnames.never_cleanup
are emptied in dbus activation environment and unset
from systemd user manager environment.
When no compositor is running, units can be removed (-r
) by uwsm stop -r
.
Add compositor to -r
to remove only customization drop-ins:
uwsm stop -r ${compositor}
.
This example does the same thing as check may-start
+ start
subcommand
combination described earlier: starts wayland session automatically upon login
on tty1 if system is in graphical.target
Screening for being in interactive login shell here is essential
([ "${0}" != "${0#-}" ]
). wayland-wm-env@${compositor}.service
sources
profile, which has a potential for nasty loops if run unconditionally. Other
conditions are a recommendation:
MY_COMPOSITOR=sway
if [ "${0}" != "${0#-}" ] &&
[ "$XDG_VTNR" = "1" ] &&
systemctl is-active -q graphical.target &&
! systemctl --user is-active -q wayland-wm@*.service
then
uwsm start -o ${MY_COMPOSITOR}
trap "if systemctl --user is-active -q wayland-wm@${MY_COMPOSITOR}.service ; then systemctl --user --stop wayland-wm@${MY_COMPOSITOR}.service ; fi" INT EXIT HUP TERM
echo Starting ${MY_COMPOSITOR} compositor
systemctl --user start --wait wayland-wm@${MY_COMPOSITOR}.service &
wait
exit
fi
Shell plugins provide compositor-specific functions during environment preparation.
Named ${__WM_BIN_ID__}.sh
, they should only contain specifically named
functions.
${__WM_BIN_ID__}
is derived from the item 0 of compositor command line by
applying s/(^[^a-zA-Z]|[^a-zA-Z0-9_])+/_/
and converting to lower case.
It is used as plugin id and suffix in function names.
Variables available to plugins:
__WM_ID__
- compositor ID, effective first argument ofstart
.__WM_ID_UNIT_STRING__
- compositor ID escaped for systemd unit name.__WM_BIN_ID__
- processed first item of compositor argv.__WM_DESKTOP_NAMES__
-:
-separated desktop names fromDesktopNames=
of entry and-D
cli argument.__WM_FIRST_DESKTOP_NAME__
- first of the above.__WM_DESKTOP_NAMES_LOWERCASE__
- same as the above, but in lower case.__WM_FIRST_DESKTOP_NAME_LOWERCASE__
- first of the above.__WM_DESKTOP_NAMES_EXCLUSIVE__
- (true
|false
) indicates that__WM_DESKTOP_NAMES__
came from cli argument and are marked as exclusive.__OIFS__
- contains shell default field separator (space, tab, newline) for convenient restoring.
Standard functions:
load_wm_env
- standard function for loading env filesprocess_config_dirs_reversed
- called byload_wm_env
, iterates over XDG_CONFIG hierarchy in reverse (increasing priority)in_each_config_dir_reversed
- called byprocess_config_dirs_reversed
for each config dir, loadsuwsm-env
,uwsm-env-${desktop}
filesprocess_config_dirs
- called byload_wm_env
, iterates over XDG_CONFIG hierarchy (decreasing priority)in_each_config_dir
- called byprocess_config_dirs
for each config dir, does nothing ATMsource_file
- sources$1
file, providing messages for log.
See code inside uwsm/main.py
for more auxillary funcions.
Functions that can be added by plugins, replacing standard funcions:
quirks__${__WM_BIN_ID__}
- called before env loading.load_wm_env__${__WM_BIN_ID__}
process_config_dirs_reversed__${__WM_BIN_ID__}
in_each_config_dir_reversed__${__WM_BIN_ID__}
process_config_dirs__${__WM_BIN_ID__}
in_each_config_dir__${__WM_BIN_ID__}
Original functions are still available for calling explicitly if combined effect is needed.
Example:
#!/bin/false
# function to make arbitrary actions before loading environment
quirks__my_cool_wm() {
# here additional vars can be set or unset
export I_WANT_THIS_IN_SESSION=yes
unset I_DO_NOT_WANT_THAT
# or prepare a config for compositor
# or set a var to modify what sourcing uwsm-env, uwsm-env-${__WM_ID__}
# in the next stage will do
...
}
in_each_config_dir_reversed__my_cool_wm() {
# custom mechanism for loading of env files (or a stub)
# replaces standard function, but we want it also
# so call it explicitly
in_each_config_dir_reversed "$1"
# and additionally source our file
source_file "${1}/${__WM_ID__}/env"
}
Inspired by and adapted some techniques from:
Special thanks to @skewballfox for help with python and pointing me to useful tools.